pax_global_header00006660000000000000000000000064152006711230014507gustar00rootroot0000000000000052 comment=db1f38b03b173984ae9ed3abeb9750583c9bbd91 hyprshutdown-0.1.1/000077500000000000000000000000001520067112300142645ustar00rootroot00000000000000hyprshutdown-0.1.1/.clang-format000066400000000000000000000034161520067112300166430ustar00rootroot00000000000000--- Language: Cpp BasedOnStyle: LLVM AccessModifierOffset: -2 AlignAfterOpenBracket: Align AlignConsecutiveMacros: true AlignConsecutiveAssignments: true AlignEscapedNewlines: Right AlignOperands: false AlignTrailingComments: true AllowAllArgumentsOnNextLine: true AllowAllConstructorInitializersOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: true AllowShortCaseLabelsOnASingleLine: true AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: Never AllowShortLambdasOnASingleLine: All AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes BreakBeforeBraces: Attach BreakBeforeTernaryOperators: false BreakConstructorInitializers: AfterColon ColumnLimit: 180 CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: false ExperimentalAutoDetectBinPacking: false FixNamespaceComments: false IncludeBlocks: Preserve IndentCaseLabels: true IndentWidth: 4 PointerAlignment: Left ReflowComments: false SortIncludes: false SortUsingDeclarations: false SpaceAfterCStyleCast: false SpaceAfterLogicalNot: false SpaceAfterTemplateKeyword: true SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Auto TabWidth: 4 UseTab: Never AllowShortEnumsOnASingleLine: false BraceWrapping: AfterEnum: false AlignConsecutiveDeclarations: AcrossEmptyLines NamespaceIndentation: All hyprshutdown-0.1.1/.clang-tidy000066400000000000000000000072071520067112300163260ustar00rootroot00000000000000WarningsAsErrors: '*' HeaderFilterRegex: '.*\.hpp' FormatStyle: 'file' Checks: > -*, bugprone-*, -bugprone-easily-swappable-parameters, -bugprone-forward-declaration-namespace, -bugprone-forward-declaration-namespace, -bugprone-macro-parentheses, -bugprone-narrowing-conversions, -bugprone-branch-clone, -bugprone-assignment-in-if-condition, concurrency-*, -concurrency-mt-unsafe, cppcoreguidelines-*, -cppcoreguidelines-owning-memory, -cppcoreguidelines-avoid-magic-numbers, -cppcoreguidelines-pro-bounds-constant-array-index, -cppcoreguidelines-avoid-const-or-ref-data-members, -cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-avoid-goto, -cppcoreguidelines-pro-bounds-array-to-pointer-decay, -cppcoreguidelines-avoid-do-while, -cppcoreguidelines-avoid-non-const-global-variables, -cppcoreguidelines-special-member-functions, -cppcoreguidelines-explicit-virtual-functions, -cppcoreguidelines-avoid-c-arrays, -cppcoreguidelines-pro-bounds-pointer-arithmetic, -cppcoreguidelines-narrowing-conversions, -cppcoreguidelines-pro-type-union-access, -cppcoreguidelines-pro-type-member-init, -cppcoreguidelines-macro-usage, -cppcoreguidelines-macro-to-enum, -cppcoreguidelines-init-variables, -cppcoreguidelines-pro-type-cstyle-cast, -cppcoreguidelines-pro-type-vararg, -cppcoreguidelines-pro-type-reinterpret-cast, google-global-names-in-headers, -google-readability-casting, google-runtime-operator, misc-*, -misc-unused-parameters, -misc-no-recursion, -misc-non-private-member-variables-in-classes, -misc-include-cleaner, -misc-use-anonymous-namespace, -misc-const-correctness, modernize-*, -modernize-return-braced-init-list, -modernize-use-trailing-return-type, -modernize-use-using, -modernize-use-override, -modernize-avoid-c-arrays, -modernize-macro-to-enum, -modernize-loop-convert, -modernize-use-nodiscard, -modernize-pass-by-value, -modernize-use-auto, performance-*, -performance-avoid-endl, -performance-unnecessary-value-param, portability-std-allocator-const, readability-*, -readability-function-cognitive-complexity, -readability-function-size, -readability-identifier-length, -readability-magic-numbers, -readability-uppercase-literal-suffix, -readability-braces-around-statements, -readability-redundant-access-specifiers, -readability-else-after-return, -readability-container-data-pointer, -readability-implicit-bool-conversion, -readability-avoid-nested-conditional-operator, -readability-redundant-member-init, -readability-redundant-string-init, -readability-avoid-const-params-in-decls, -readability-named-parameter, -readability-convert-member-functions-to-static, -readability-qualified-auto, -readability-make-member-function-const, -readability-isolate-declaration, -readability-inconsistent-declaration-parameter-name, -clang-diagnostic-error, CheckOptions: performance-for-range-copy.WarnOnAllAutoCopies: true performance-inefficient-string-concatenation.StrictMode: true readability-braces-around-statements.ShortStatementLines: 0 readability-identifier-naming.ClassCase: CamelCase readability-identifier-naming.ClassIgnoredRegexp: I.* readability-identifier-naming.ClassPrefix: C # We can't use regex here?!?!?!? readability-identifier-naming.EnumCase: CamelCase readability-identifier-naming.EnumPrefix: e readability-identifier-naming.EnumConstantCase: UPPER_CASE readability-identifier-naming.FunctionCase: camelBack readability-identifier-naming.NamespaceCase: CamelCase readability-identifier-naming.StructPrefix: S readability-identifier-naming.StructCase: CamelCase hyprshutdown-0.1.1/.github/000077500000000000000000000000001520067112300156245ustar00rootroot00000000000000hyprshutdown-0.1.1/.github/workflows/000077500000000000000000000000001520067112300176615ustar00rootroot00000000000000hyprshutdown-0.1.1/.github/workflows/nix.yml000066400000000000000000000004441520067112300212040ustar00rootroot00000000000000name: Build on: [push, pull_request, workflow_dispatch] jobs: nix: if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork) uses: hyprwm/actions/.github/workflows/nix.yml@main secrets: inherit with: command: nix build --print-build-logs hyprshutdown-0.1.1/.gitignore000066400000000000000000000005421520067112300162550ustar00rootroot00000000000000CMakeLists.txt.user CMakeCache.txt CMakeFiles CMakeScripts Testing cmake_install.cmake install_manifest.txt compile_commands.json CTestTestfile.cmake _deps build/ result /.vscode/ /.idea *.o *-protocol.c *-protocol.h .ccls-cache protocols/*.hpp protocols/*.cpp hw-protocols/*.hpp hw-protocols/*.cpp .cache/ hyprctl/hyprctl gmon.out *.out *.tar.gz hyprshutdown-0.1.1/CMakeLists.txt000066400000000000000000000033321520067112300170250ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.19) file(READ "${CMAKE_SOURCE_DIR}/VERSION" VER_RAW) string(STRIP ${VER_RAW} HYPRSHUTDOWN_VERSION) add_compile_definitions(HYPRSHUTDOWN_VERSION="${HYPRSHUTDOWN_VERSION}") project( hyprshutdown VERSION ${HYPRSHUTDOWN_VERSION} DESCRIPTION "A graceful shutdown utility for Hyprland ") include(CTest) include(CheckIncludeFile) include(GNUInstallDirs) set(PREFIX ${CMAKE_INSTALL_PREFIX}) set(INCLUDE ${CMAKE_INSTALL_FULL_INCLUDEDIR}) set(LIBDIR ${CMAKE_INSTALL_FULL_LIBDIR}) find_package(PkgConfig REQUIRED) pkg_check_modules( deps REQUIRED IMPORTED_TARGET hyprtoolkit hyprutils>=0.11.0 pixman-1 libdrm ) find_package(glaze QUIET) if (NOT glaze_FOUND) set(GLAZE_VERSION v6.1.0) message(STATUS "glaze dependency not found, retrieving ${GLAZE_VERSION} with FetchContent") include(FetchContent) FetchContent_Declare( glaze GIT_REPOSITORY https://github.com/stephenberry/glaze.git GIT_TAG ${GLAZE_VERSION} GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(glaze) endif() set(CMAKE_CXX_STANDARD 23) add_compile_options( -Wall -Wextra -Wno-unused-parameter -Wno-unused-value -Wno-missing-field-initializers -Wpedantic) set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) message(STATUS "Configuring hyprshutdown in Debug") add_compile_definitions(HYPRSHUTDOWN_DEBUG) else() add_compile_options(-O3) message(STATUS "Configuring hyprshutdown in Release") endif() file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "include/*.hpp") add_executable(hyprshutdown ${SRCFILES}) target_link_libraries(hyprshutdown PkgConfig::deps glaze::glaze) install(TARGETS hyprshutdown)hyprshutdown-0.1.1/LICENSE000066400000000000000000000027371520067112300153020ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2025, Hypr Development Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. hyprshutdown-0.1.1/README.md000066400000000000000000000007131520067112300155440ustar00rootroot00000000000000## hyprshutdown A graceful shutdown/logout utility for Hyprland, which prevents apps from crashing / dying unexpectedly. ![](./assets/preview.png) ## Usage Just run `hyprshutdown`. This will close all apps and exit Hyprland. See `hyprshutdown -h` for more information. ### Notes `hyprshutdown` does **not** shut down the system, it only shuts down Hyprland. `hyprshutdown` does not work with anything other than Hyprland, as it relies on Hyprland IPC. hyprshutdown-0.1.1/VERSION000066400000000000000000000000051520067112300153270ustar00rootroot000000000000000.1.1hyprshutdown-0.1.1/assets/000077500000000000000000000000001520067112300155665ustar00rootroot00000000000000hyprshutdown-0.1.1/assets/preview.png000066400000000000000000004125341520067112300177660ustar00rootroot00000000000000PNG  IHDRbWZ IDATxw]EiݶdIH HI RB &tPM@@AEE UK BB  -i"Br9grݻl2~l=3L3<3hܘ1`0 !K=C0@=Oxղ0BU`0 0 Bd~`XZ`0 0`0 uc01`0( 0ƨ` &p}0D 0_۶JQJ=BA'O*b]d2!/Fa>OX6 `7! RB^ɾ6Fﺮ8󮮮d;mYcR_Bnh!祔Jm 1a M}ÐR,/ ڹ(B]2 ,q6t=ϗQ}eP<Vpq\ϋGT_Qss]lxO)E׍Q*}q]˲x9C!1ch0 '+MkԺWjIdkaPYiAda*_(,˪Y_kJJgzԊ+1&8תLژ=Hq}%qH[ !=G1:05/fRJbFRZz<)UGLba>ԲmTYB-˲' Gy}6c}cOJFiȹWH)#`,SMoB+zMeY>z-8'uԉR`Pb YL.ÿam8kɿV8/5-ɰj{eY*.NW} ,eEFJYx4>ے5@amK& Y#IT*iPJ)eit c=RRJ$~Xq-Ҟc[ 7F8T@5@(GyU*! (-=ZQT,Z٬A[cAhhض A!.c|R UQ Y"l0 oHaM2JKR1hڀ~"%;!`ӨJvORJR Aa \1F( 9(ڤA7!A$lRjM\D6~ebGm7Q/ <0p_І k ¡Ru; %ض072 _0PI]m5Қ:Ƹ%{p6l+B>pV)T,iTZWRs4_BVJ!(ySJ+C-ұ1uND}?N9w\q}#~}xFOlOZʜ!P c75LS |Yd(8bi"!dYVmE;z0_qHd2a*0lq~Yu13gSJ|rYG"c7 Q !sh?mX&Ѩc!PJJY,0(c!RAPvjm۶#T{vJ!пJOX6 H:oEkimI:&ttt=IWgg4mW*d}unjʵ'kɕ^fu_|Ï8bԩw7tv𢋖/_7YVr}t=?j(` Xj[y>sߧLz?G?#9d}?~RL6}{zRO?{~ 쳥ZfG}̙3G…+_kMKL]8`]w8q"}3ۢ:3|>xѣGrmpn_&M|9,?. CMq]۶IJ8 !JRJiJ͵I°{gV9TcL)*;PJ`Y82vrlu]!eRTF$l˾aH)eJRJߧ!d; YeYbQ?aR!D߻0 CBc,N{v#k$K^~YA+?>w ͞?1cL9ܖ[o?}vĉN?rSO:u/}7K/9sf>F5LbRʼn'~䑇7㯽uQJ `v~ Zg'7bK/z 7K҄ ?_y؂qƆ&M<7ߌ.:};tܹI'O[_t7֪_^wg]U3fL&I-s˖-gqO:?Q ^zi:[>R8Fٛ\rzk;>#;߯t^}u)S1#Ϝ5}˲fΚWg̘-oo0@ a !/ Jgj#\P8z% `|qsՅ0 b4!3a y̲ (\LbE{vP*}{3ttthoBD5Dz",/4$R}ʘvѪ!DXaڎ@W_h0_c!e6miir)v8@0$~.c@PÐ28N$|TG )JB`%<$.RY7$3#\}E 1JRq (R4 ι */W^.?{gV3N;s 0 -rV6_͋~;ןX(kY64s~gk.n]vu/^pt SJOwvo1r䔩S_|w~{ԩ#F>k_>h ~"Ԋ(\L$ph݊C hZ wl!8 RF2w9+#Ƙ6F$; TG()c1 Jj'qlKRsㆴ0 !`/1. Bp9~fΚ,ZGş+vygxVhw^ӝw[M#̟N f?`?1a#Fl&'x. C"4)Tbۥ6r !k$2$+P(dg\>vCNN6Æ j;NX,Kԅ>ѪdC-i[)Ow-YC [&B뺘BJhV˲RX POLv}?Kc,y&[?^JWXF5@(GyU*! (>Z+ߔE+-;3}q]a6 J6P,?N8ҙyk}e Qc v߽~[~O?Λ74NަzҤIl~")*!d뿲dɇ|ooll֎;J),X0rĈc=v֎;;Zxq=gtZqVK %k}xsVH|WTN# MQr W ! )\`Tq\raXBa7=ъ|*RKPFiI6HN!X4UH}Zݔ\{2ffeUeQ(:q)NH_;0ucҐsJR*M4zA¦\sfYeL>!ߵLOXDտ,`m }w<3&۶wuכU-Tщ'^xAt~__wܿB'H|,<[uWVo7pgx[l//_le}[mJv1 ; R2=G|BJZʕ*(b0,A@;EH!pYT@Be@(` "a ( }" HFQ~?wbhݢJma4bo(web u_VF1UT&yV#`t:-*^Jj{ԚJ!(c!|Ji{۱~}?N9w\q}#~}xFOlOZjxwƎ7aD(6O\}B|>7~ԩSgRj~i={Y]]]cɆ՟,ԓO=5+K\tVp~~umo1bĈ-ŋ_78?bŊ?tӻ;}o?xc{{/f+^|ᅯL֛o[~`Y֛є)=cL1rqƏ9sQG[ G]w~GN;[_6m._,_pϾ򪫦MvO4ab*_dIW,|Qmmi\7);6nK)nZ`0 So0 5Yv!{3gl6Ͽz׼wwuѓ&O=f;o}Lww3O9I'ێ>9sN?ͷb~>b)S9?m_\}e[[V[mχj;>ه>i#G['ϛw1U=nA`0)h\QY`XYo;w9#ƽ#BWlՖg?~F5$`0m .?af}C7b?_-^ a`0Vr!ri>wsܗ7(|ó{0V``0 _&mY:0ƺ~ @  ðdʡF{RJ=WAPH:xO՟T*,jǪ]d2ԙDa<h &&цT`@Hg7VPy8P UVJ:k[ umW(ؖ/R֟uCy}l6F{G(ERRbNKϋL5_m۶rww)B9oMB&NiK^w絴R=WhiBHGGޓtuvFӪ/* LPA[Zb r--DT,Wz*P}5FQ 0 3]6mц e$F(#Ѥ{v AttW95Uumۆ&)͎8+(JRJJiٍM#$*SWR!9ƘRZv=0c̲mUq5Ilu]!e VOX6Ce02K%)%o+fYeE1@hk"]Sa!!1J } uU(n_2 & 16p["jVk i@F h3| xXGGR1v/"J1|߯N_۶UeVT*EC))0, @l۲mY@rG]nimEUۊ anRvvv=3s1=$qR*= *B%alۖ\KKe3Pgg'4qq=]NӌR'\[I0P{*1^*E JZKxH\6 >|c7 Wi_J8ߛXLɿ 20~LKv/ e鬯51jT)}g#97d}22̲x+}`z;ZQ&r מބXU7ނsRO(6׃M - CFvS ]JCbK"hRPP9 |hD< XmPmO,Kԅ>B|޲H1 &g[v[(mɄu1!XoR7B)1])LސRj'0ƒsnY&Ti5JYj|$BkTCц Od g12Sx}Ĭ$ql"yRiĐ* c 3<ԧۂ^ֺ]YpMP1(-PJŠiZUNJb*F=I)J.2feir\q͖|wwR I60ucҐsJR*M4zA¦)q*d AJdZ&vavU\Wc7 J 1iǿ6wg/(^}瞁JA4b+ed V,A\BV~'{[Ѡ&ۘ %ض072 _0Pͽ> qV#h/)c[ ' : JJ>u8SJPԴO)Eb1NKʬ׺ؤJT !@\o*B_YJ(!T:]&:q]qa=g>iqt*sKA 0fWVpT_G퀐(0.@d)`P&aqu8Jʸ!dYVmE;z0_qHd2a*0lB8Ԣ;U)/ܨ[oI}Ʃay=>$>}Vۆ,F%% [A]Q@<26;B # lV WF(W"gyXA+%v%aW`0GY;9}s[nN;Kez?任.}4F_|~0#rMw04aB{3"E\*(F7  W"j TaMLs`0%hQՇ̹uYs?EmVǏ?<٦L\̜5CEuwu];ʫ~GzI=eJmFb3cp:=`TOvooԞ'y~_y,Y{|{wW:ux"!/?gUnfmo;?~~=?jˮЃ7'A}ms^_r->;vlղUj7'Mt5ꗿ~ƌs9`>y]æ]z|v- 8q_(vi|^|v{1rdeu\V}aA ltOy䑙3g"vsx`矿_9sY/^ҥw~?^|:GYgPxex;v,y˶%wZmŋ?cxg p$(.]%K6nZeRYany}dԓO:3f̘_{LóϞ2eJ\Mzkgy93wbL)K.9l9`fohcPA CG0Vo^u>/;Ѓ瞖e=W^ _ҥ ɫuܣ9fԨQϛW+SYM1ò?C̙?\ Nwue9gyA~) ]z'R?xk`]v9cUV'㏷׹B` au]q]]]ov۲IJ}VKKK_IӔBihhTVP皤t:MzgWggtctT,F9 Ηy1큠nJDqcUF.V &TIӔ`dmd2Biunjʵ'RЕ^^gg[oO'~xp ??*0aon{Րʼ=I H)76 aXR︮m28㸮T*I))egM#$ æ:YeYQ c OK/pe97T* 4 ',aRJegyBwMaX*nBFyO=gAp٥iӦY=Hz !c0 Ø/^*e1ѡnA0ƤT*}VJ2rTFC))0, @deoFPoոFնab$mu]!%s1=Vd۶mGJ [V)XODd">K*3Vld0Bt& ]V!%8畞e$<ԞJJʐsJid%<$.Ryb(9/ Qxhk>alֿ:],]wwG>]*Vh QJt=iF$X!c7 V@J)\k-aE^vrCײ4{,+}9,۶ !RҨόV[ӪVeY"Rb;NXq1B/dَJb9cuݲRBjK)E!s4%Y)ej[]+e%wc,, 1fYmD= Sz%D/j &yeA㸞K?0J;kYV8RJ<'x&`0 Vcc?%; ˲tך0Y*b ` ε*A.F-Td0DR!ߡfA!j_+e=:Hbq}*Bm,v|w7 E$ H:F>a*!纛R`?0=bTSkEF׆o9NR1aJF&ϏaM' X/6d/`p(U((A#T|RU%Bh.0Peުd2B:]}"|޲H@פ&>ے5JK%B\ń䍐jF'FJӄ|wwwS~Rj?sm,זLcB!C}bJC)!ڀބAݛ;M_荮 }DdZZ[]˶(>6lb9W]sG9ح}ΆK IDAT9͞ݏ=[ .o$朷tP=5?8C3YM]JBqD b>6 BA'*SIK'@" q1O  wB'ݔt5F'ض_T*Ң5> PIIk8:9 h4jN%_O);cBYJ :褖RP* !2j4i:BP*>V Ӈ[,+cc0Ƽ7PԿ6B>MIq{uF֞`l;tʔT'tҟ7ܰf M' 69s>=/ϣ=SV>Oƌ'4Z1Nae^wݪT3zg*_ĉRʛRjku#a Q PPE Jv:eU\:>hOr* yxo+ WUaGm+[QwunX =.q! !d۶vю^h(z2LJx0 -$.L+3JR'PTmR)@( $ݍj=%6D} :f:c>Лd1z[l3>8s+{C˖-;'xBV[Uƍ{N8KhGK_ge˖U=ztVVӧpѣTOV]1cԩ?8ZH2]kۡ\ P僙E"Dգs7L FVC!wrrh* M7*B-_<<Ƙ B/O\S*J"q]UlPe鄌AU_#t6}(b<>U6:_:R'+CA+ PmWصnxZOՑSZZ6 BoB=,#Ġk6yTOMC)+^Ycq{l+3|7?]O_9\_y/㥔=O?LԈ#2>{'C_G=l?x3rF/ʴieY喨T*u~v)]lګ:)W_sORe»d>?[zݵ>xq||ƍwa>njyfѢE w!17sGzxg·q+KD2;Ci_<.R1'?YyoA嬍9v}8cw>#˺6h=;׿ Ͽ6z !k3w83==sɒ%e+Z1bD5-K_?`]X {Z U˭rGۿo>po z_ҘuG/sk⦅}ٗ^z >wU?彗nN-n, FD[TDQ\f:pb03."4"4v^Rʻ}󖛗%ɧ?ɭ[έԩS?6o|=ƍJo^qI-ܰ?0` -E]wareO8aӦMo|qO?ԓc_"#@۷m3a}}}/4 |s?aGGF^}1}~B+Ld8Y_شIkIwqwfQ(}`` Io_ye$6n45K7ZZ80^-kqV-7+_s]hQ+W5Ob:yYoc!)xss̓-_ڷŧx|e}n8uw~g >Io;a {˛/ <'O{|'~p@a#7ɑJȿQycskF~S߶ߌ{q+W6jebU2uFGFk֬3{Kae}VxfݺC;̘qpG1o`-Z<߮]k%Zg}$ji>~׻(}k|[晫VZ 7OK;ym5wo׮]~׾>j5ٰ񑧞{תw__}I_~СY6R@Wrؓn ڵk_Ao?+&R0>O?_hs(_>X Pভ6?ޞ?Q{GwϾkl{=q/:>MsubYXbLe˗o4oذg.ַq!w|^{o~} QsB~Oن+цV6_97SN;-(),YBy纮;qcK. @O6lm۲g-7YgCAK~7tSVTO0]3rhŊ{Wv54j-\46md\&6nܨs=|H߶uB߿y{T,Moj!; -ǽi[ &׾߼ysuo[AoP0Ck֜q<9<n;No~q75Q?C=,Ūe,[lpm 7/}~vӦMկ8_0\Y-7+ضum޺ro_uO~0 wl߿9re 4ߟn4r7/^oFjmzʕzq}۶f+lݲe-_uٮ 8N\6{Pzuץ^zE\r{w'w~QG.ڴiӆ^xᤓO~fhҥqgk~o4v?8W41m޺|H\uYZ]t7a|r6RK/yQJn>msƽ;cS,^`{zCCY~}ky0.;sG7NG򱏝{9R׽Gs?_xjsu,\:Y,yC>/Z>pѢ+WWFٮS,y49O?Ԏ/70T*\tѮIM:\=i1&d{RJ}78rS~S(-#|R 9C{{Ed#3mrjY, m>"Eb-j\H[XvW8k8 ai9Y=s\W 2dq\dRJu)Ds\!TVMNI%&+|Eqo-JbΑ4ġIjaOR* JR6Ip6^I9&V)4sD(2g67̕RD(M%P;1B^s$ k![?hX-Iz:ȹی6مCbHyʕX%L\a<PY$Fьۗ( P?Vw=q캮yR0 RRBHW:I&=Nq8眧cY>$9w],̝JCNO9fv^2Σ0TJ9osB"I$!0 b,pP,  j!` a%6eV 핞 CQ7TB3VT֌1q8f-)B1EQ9}JR…B2JIR  zz q@C\F&zZse[i$I0FCRJ ;yT`,c8㘭 uǦC,<()9I=^u#H)=YG$IJ==1eMAbK%s1k0 "9ik =c Hƫ턇iS |gF ArF念Yu%LR07q]}X'|1tfY,]םp4- =Na^*\dh Zb5 @B,3!yک.PJF]HVEQT.[V3ZcvN 3(B0QRFQ$4jVoooܤ`25tdծyԕnXFK)(RJ1a6eD<2U(cѪKus¡lF)g(=ߧ%svCɩ>\q-ʪ7ĵ"_ft"STõ8- ƞ9mTɖH);tI1vhTPrS#%BHfBI(ih-y&tS&xO;lƓǸn !E#On1w<;Yc|zPBt3g" (ʹQSfsq@(cH3{ݕʲwCR?ڇA$C)R@#@ 7B(rjo&]"-]DF6˜nifevB\#G$@P`\sS)a1ɝE!S>H6E]ww{2JCsNtƴJg~E`ܓօB+b(KBB aw0c,dz:zy1Bi"%Dkrc!q,d2[q77H]I3PJg-@+_*<(Y ٢QeU6j hW;  himQa(jsUQZEUjs96h4D8-2GPϪ!y8E;aR|P*$Zg+&I}cDO*(!R12mMc8TV ODJIw(r]qcMXE!#RxO8-J1Jf7`?jZ|ߜ !g(RuR^|shY+c8BE bƜ1[d8BʆK3Y3a7gs097̫er T5gUTnnhO =R !`i|M0*Z$dTvjETJirdb=7#: u;QVs&O;V*nJetbu5YjykQי%|E(L%BiCl B%RƘIqJ% 3WVbF"D?GZ @p砵z.c&PEAùA)+8I5q̕TqAٔ^4Unr&0 @%GqmI32(PH:i f/Mwb,Ad€ FiȒ~(R-r>F%&ݎ'V?EckY6ePV= i턒H 2M.GX,ú[  P ʄ5-m`܁HFh6b u?ؽU-32jmF2 G~ :3{ v9mX,Ha֑Z,9[,e"1_л?%sZ4D6ʥb oX,@`RP*2bX:@2o?&dSJ}78t, ?) ;ZrTB w9@U'{{ET3 GiSb]@oX,<g+GQ$ qRJds 丮1s^h.B"I$!0 b,XL._ @ƎZba%30)@@ufHP̵ChPOβS XRZ3\lj㘍 XE8ZGY((c,'I-R17p hr 罋Rύm$ I)5<<8yR)c1N$8cZ8MLYxT8cJZőr97wWNۛqR #4< $ B,{M$I(cKjBqBT )1ZR۲&)s׭XNjjFʌunl9C(M0 Tj .Ƙxv3j$B4/!3yYxK!H>?&|mH{Z,uFtAؚ-sոk?PcO댺0!R]}R~R*V9穑!$ d1Mb¶T%B<Äi5pBq81?tvc'Z-!xrSe}J)kbn^mmvil9RJ:>6 3[let8]$B 5TcmgK$$rJ)MUw~`R|p1>JT]0yqRqS/E@혰R\}_4C‡a1f3ƴRa#3^9rB0ƌv(ĄMEsV*iLxr9CYV ŢiVshY+ cJdz'"IL@8IEL-Bi6/$q\2WSq/e2fSzF,kf(c&˜G>ķv X,g B%0F"a#f)|,<ֲê\R4UHUiH#Y,e4XvG$@MKsZ(A.BkX9X,<{j8@ D2F7;`o#a mX,bW( &6W>~uJ9ֱ3UŲ1@2?0Rɫ{a\méKe}ۓ-bX,!P? s@eBhTܙeZ"螭-eKYŰX,D߳5.LY꿏8oGb{,eVрP=bXfIx3m;~Lʡ({};)cq]㙚 KZ9 |hǎ梖\*Bó*+9LZn-l27Q~9b|-7nyR丮ù:NyIݡjjrNG !="JI) Pv3BZ(/)c!PJqܕܫRJ&d_y>g"b E]5)"!R5* xjXJ%%P;1B^sŦ0zZh>?>F%&+%s :TzDYv,,zhYug8 l2LW#hFu&=W"YE]PqS UQ Ԧw{~Vy@fu=aA EX|^?~9cIH)ծ2JʠZ5DV]#0u9ivE)RahZS& î,w 9Ly(jA`RGaX (BbZae!DQJQ._"IL)P,vX|Gk72.ƱңR } bjbd@B̏.ːjJ˟Ro|aPU*5cu8x_JP`EQT:iqHߣ*\((cƘ'I-R17pᎳ'.f,42{-2ͥ-J+&I16RjxxqRc!cHqqi2|t(*ՅΘRbqdd\.s͝9I˷ʗ{{8nX*a)R\;yB(!KC1 J$ 턇iS VCk0K ZΨNļ)#dUx"L1( =g&#+(bu G2Aec$(K(r1vg;aj"\@d$2j>f Bڵ1]uE65w R!Q$jQՌ3t{¸iX(B0QRFQ$4jVoooA~g:!V_}u$Ї8<<|e-Y7oI[ T+ݩF#!!qݰVu1RFQb9ZYN:*1VJzO8^`B'TC;v 9u݉]|PzO 썠խPT^F+9|7 $I!z=!zFM!cR?a,27FT*"8睨˓9 z'bsPj 4@0h_K[@& v"K |5zڟXk>;(F1cRM  °ChZ).ZԻhQoc1& VG Fa`}V3MBOzK_zwlK} b~B˞ZVV(J.A!u !R)' ؔNNPREcc9mUZZj;\7i(Mð[ҟ$II/fԔIh^B$I7{H!@ A휣 Ғ?I(%Qo"FFh4(,&B2BTT0M7Ҁg+{*\gԅ RNV1fTPrS#%BHfR9ש~ϻ޵hJ!{w Տ6h͖rBy1!=spb1~,:>P(BCBp !11w4;85L_l !JQѭ*BBtL5eZE߸rt~"ǘT´/8S($AJ1B wcNݘ-2Jv!eÎ9@(r]q-`.o쾄931"psf=G 3!swkgC0@*rXi2ƉFPЅCS:f'sIn&fd` ~v?R\?٫cNx߰6sp1>*s bsJMNS,jͱMկyG>/5km`7nBDZz֭l>KH o?餵?N^Z<5LGc8gӺc ʺK!&V`PJ T8TJ8]rMZ-!9вV38nqt(hZQ$"fcPJa=NuM!s9jbP(c6noHL@JUw;r&z#bL?OPM JiVrRjlYfmfg_i#B/k[o%[2i2io/}+<_/}ӧ?ztvN8;c8W Zi9544y)Vj Y'v~Ci(V;[ΙZ hIpQ%g tr˧I!1 ռ =;K!54Q-99fw^hM _7`W_yt_rӦM'p; /K2Z,Ba~zHݎIP6}|H.XQ"sVz꩏}B;?xЌ[o=o9o~ {bXv9Z %zȚ  Zmڑ[G'?緼' ̒^i:޻N^9;gNfbXbsoxǾ/O<ӛK7nڤ>~ʷuԙV ==='zꙫVtAJ/yojŲA b,~v}߼5mټի>䐏~cB㎇֬y}ѬoM-eހf.;bXfW)6AЗ}}3Ԅb} [2Y#ƈPD0BؘEm]UGwE|g&Ty`w!P'PJ{zzz{{{- _|8n&ܧP(.ZԲ+J #|δɩeh Go泉kbPZVoBX(xڣzר.3 )ZWGGHJ$rx&9LZ]X,YҴh֣&VX$T*B8NqZ-!Rd&C0[o'Bkؔ^ibn _$Xh]KA 뤀z@A(wxsːtbr*p;Nۅyt˼㺮y Ӊ !u$RJ&v䢋N9餹*)je̯'[u=;,\噃 Ƌ;XGQV0 %3mf畣(I a-D$f6I 1:]Z#!J)J!Ĕ b珵W?qpN6@mё iAŨ]p( @1jr/8cJEks'c6R(cQuqI۠a ʘ1fIR  ."qf)˨n87qucg˶ҊI`*v<X1'IbY:c!iG'R])jAP,GFF2ܙ3^Klܶ{{8nX*a)R\;yB(!DNT򜞇C1 J$ 턇iS VCk0K ZΨNhOQjVzB2Ǭ0|1tCbiטD^ZSXbt}DJ;5#, ;J¨ Ij(rjc@C=c aΠnX(B0QRFQ$4jVoool4չRQCTvm7pStPuZ \5XJER1渮kO1VJz8cNdBaR#koB a7LF csuZоs}JHfoe &f$ !u]SM5U1WJR|R(\ww.OvZ2cf`Aȹ0}j-Lr_Q7󴀘i#N;2O1 "c ceT|G 8:@+jm3ƤF;|d-ujz0R$!Bd7=$.A!q!R)' 68Զ:V IDAT1SQ$IER8Z)s׭x'ժ)3ằ] %4¬+R~&`Fg_ͨ)Ѽ0s,B8>f- vZy.*rܣf Y- @$0]4BU~;?~DNK(@A@@́@E#YhP΢=cT0QcO댺0!R]}R~R*V9穑!$3N-Sv RmƖL<2#;8W!$$4~,:Nej)LE=0N9'0fMnZeYQgQ8|$B8 !fDAQJ{'_i7LXw386n q2cfRYv4&bĸ樂.G=L'3s( 1y  HABS(EhghMQd( (rƴJv]dB{ֺA QRh1\JYR ήT6$cDJ:MZ fq΢qjXdo0Rf< ,]*X,]+O q@ L;rɟנ DÎqtIE :zJ qL Lq'%BNn;Fop07u&80 $QJ5X/VpdumA6cZ 0ƝXٝ Q.N>hm V#`B2g>sƙghuW&@qQWLyj'֨ǝFha8BU#@Cf 5Av41ʪ$f2gC$@ER亮qaO$!㒅byD1a[7͟◿|>hl驧_bqV19V¾[1 ^ynQGW|[B9~39~{Itt뭜ŋ_nkO<$ι)%]V(6oRT*ev3Hzge˗oٲ!c:^^1V,;rR(u]qFm}Ji)[J2V(84_1ƌ8ob663$OSzOt!ιyBD+[,]!֚ `sZE.&ݓ%|DK0p&]b㽺L"T\SN_¸  l5h~jE,D-)c=GcBHuto /A Y!aaq-]txx)voi:Mj*t]""s 0B㑑;9>1cBfoj pRZ jehȴҼRp@igOrQַn:{zz};_D}:?OOWRT׿Տ#mXk9 c=L8=~pMK}KnƯ~k.Y\.k_VX{_4yIutd㏿馛~ٗ%p!lٲ[lYeh88n3^Azއ?|]w*G{ZSN馛0K,sɒ+W{>䐓O=?y#8\jwue~Gy$q]Gy}{O?]T=ƙgV*\ŗ^z׽za###K.]}\wG> #GQdq2<eZT*;+JRdц^jM4 Y5)DjЎfN ё[ T#4u!%ù}jfN2&O-Vg82 @\{*BUp:f򟍔T %sK2zU 뮽1ve=S}e˖}zыrQ~/[nŋ_ g=S/N7?䌽 Ї?j;c[^x{Gx_Ͼﱇ8?+.Vew턐۷SJz~ťRm'-7c-߸9}OѧBǯ\f͚G}֯_?00PV$I&=wUTgYV$}?@9Sn]/~WXx?i}}.^?i}O>蠃^u7|˖-ܰs50w߆7~z!Gs~B+Ld8a&gz}ߥ}}V=찍>qشqG[, Uy[v7%Xc5k8gy%KB>9TG}֩g=yhDG݂dz B9q^a JܹsOA 8w1YO9A4<[XSWjc{+raE1Zk HEs5&˲6BE1W:Zy9B+O0bT\5Aje1,sX]hЪj97'9?_Ts$)^Je8dBhpG\4I|jUF?€p[#K0RB-7B1&FgG+zI']qV/$:=ajfF*Y>Dv*Es}?]a( CuJ)![#drJ]4(\p\)RpAxN2DXk쫾jѸBKޢag^٥C2γ45ƨry`D2ay39|1SkW}&bRJIaŵ/6@moA3NBp!ZQ ΢e)Gn}Vqc1f~~^6`ceۋ,B8?}jl},Θ1&ijΝ;ggg9m,k4\5 㶯yJi^wQ^9ZͷapWb8씤aSq\JVUl;:܈RZΞ2Z3JݳʗNYOZ ,M(b26öZ,F,;@X*VcWZ1 I7*RФ Dw/DAhm{ R R$I,{3 =10$7]8JeJ)ƹheɬ$(w2tgyJwO{Ҕ" M/:2c.w;@$}_2cg6&cl=1*\Յ1gԎ]0Xjc ;|EWQD qK~aSqUEn*AQT!D-c, îZҟR0VTeY .)FQUugiD=2E . d寠vyc$Ū[|`} 11q kw12y;zGd賌P%)%BM{RE)%eNH!mO@X~a`wgڬ2hos.Brɢh6nmd獃Z+R)Ƙa0Uی]10A5FԢJN!syoxk1?#b%/^߳GJr,L =kbo9,P :)7:*ܖP#cLNJ.'3eSCe[۳ !abBviB\űZM1T6R/J)ι&s!K!"@ 4pPJB TRjRUz! (^E& :G1 /ZeyR17Y%k\11JYux<0dg=R׵ΩO)ńح*0Jn5`u uj/@\AEW*AhcžW*\8VB)Fh)nܮDF B(V䤔ZkBHeY.֫x#S^p{1?nToW~?B8`y`$3R9'*jt#@O+={:.&!^)Q( b1\pu\kLSiZ6$I0foeL Z1Bh) ڰ` (UK^{<H0s0]RlR|*ϩ€J4HSBS&H]h$ꓵӑ+kx<g>:Yh0Iyu%!~NHJX{ x<ða٪{<g{<~{' К_x

3NBp!ZQ9hn{\YŅRJSHƘyіcc,$e6s h(EJ)mt#h9%0FCyz#4??O)`a<+ǵR ꖇ]1S2*MUEsfXjZEQofnD)e7ecD@a1FWa JiV `hozVBտ3,SHi?"BAn`vJRZR&Iel\0.dxPHE<˔Rs!:2YF#wJtE:]wг8*-Ҕ" M/:2c cLH#bauֺwRj7iER333ckr1μ۷._cliZ1yÝ[+(%w~qAC尩*"y AEGbYF6&˲ 9\ SJc2y5U`gJR|0b9jAu ~tDE딁50ƍu5Ì1T󼹰`ᜏrK.|s}?.䒰G }iR+ RJH-TJӞ$IͦhZ: $ꕉRJ@9/GtBBRu$J)Stsr33ݎ0^fyٴ֎8o](Ji:k0Ν_Ju Qfܱ}R"##e-`,0e )]\4' vLι픒8)iz1l!K1%~lܸѥ/^uK^Y~׿2m2Bjlh4c4i ΩKe.=317?i6F롾gErs ֎PZ~а!BE?*JBBRh" Qfܱ!<T}, yhAq"x!FW\2A)T/F=~ ]`R-oY~9 So>o6K7n׼SN)E\=Wuaa ]|QwGx2ŸB,˂ pI{zd jĄ֥a*j8FQ.Ǣb(Ahcz{T_A߫ds!\"qnlPZwȍ>6\Oy]v{wysg 7!s rӼzF? 9j*knDJ2SsP@ s_E֮O(8"oFQܠjqG(c 1ι֘ԗ4Z m8䓏}{Ж-___p.;0??#ԣ>q7Moxv W<)7\wJ$(,jk#4brς<ˊ^p"JR5r%Iׅy`V1ԡH<^Ƹ8Kѩ6f3HJz+UVp`V.mGXsp,kLNFR蜴R pyܑ4yWA^k%}Ԡ45?}]3m1hJEAmc="Rh/{s/;n8X$''ܶmh~~^k?XX;/}|h8G}]DZ-S;lBtQEmW< 6څa_]UI50⤢LŊ"FݴR}l U 5Ni; aBڻ :)8ԧ~o뮽v7k6nҗ z3;Gp{<Ϛkoxz@42>TP>+[?îW!KϏn>Gn'dtvAމ?U)_.BV{9l)GGs܋{QGz衯}nᆥx<@aђKx<Ta@+wo* g=swįڟ})r2GV <s~Ӎ7.dzp:(ȇ,LG/yIG?إ_# rۿvK(9csssZX DW1c,˲k !l.ty8!1Ǐ; hEQԷo"\c:"rS/R9w 3??/PC0VJֲ@"B kmA};4 1d6M0 H4 CƗRNGZW/62ϻLyJi^wQ^9([*[w%8cJNI:x6W@E̙aRj;;;+GT[S;PZoњQUa JiV `hozV4vWB>[.Ұ23„NrH EM(0Zrȕ5&n@)UJ9 L$˲پ`=1 (y)BueZk'Ff1t'Cwv*A*Nt,M !"$ k,30DX4I?2RmLeAr ecLfy>Uk]BJ)X 1ڱ} zP+c1V18ws!n ֗æ(8y;/xawQE) ðy{yЈZ:J)c ^<^!c=.?JW_B.# ZXekx埯y5<1qVb1!Vs>b܈Sϸ鄵.&cL+L糳y\>X[/ºPKRJ(~w)%e.M 7MJ$Iz*'O9Dhos.m!@/Ytk#8o](P(cL_mƮ.4Tw Q#jXk/V>S--`Pxp$j/#S- '9d0c֥qi'ZPOqB#cL,j!K$ۚCe"a U!k)^eyR1>Zy΅Fn|u. P)9wc.{%n8kA-?+ !PяJIW6;_{4M$|f&\E[ PN׬W 6`I\ٰ m.ǶlC'CYRyg YXad"@(ng1p;KH䰦T)4eSN"b ^g\j'Yk8^e%G5,v͠ƑZ)b/<0dJRk(sy^Ѫvc\S$sP/QKkgĿ b@ !@ 夳ԏ-SHIH uyZfdj@|:X&sB+!vmOB|/X|wmC \DxI 0Yࢤ\l aG6f() &`ex,WJaU?nֺl0 MZf^lԕZk>3O]{,Z5"=@>gEѮ#@`dg9OnN(|e -u;ʿ[:5, B.wMo;K YazBRJB.j1 MfC5.\fE\e ?;81(b3JJ~ R"cLHy '2VeN874Rq'ٟ-o2诊WWWP*R)( qEQ0({sXǜs!Dǵyvr%2J7:_J\۷GQj3B0 8+7ͫgv}m0<ϊ@KP˯_Ah[+Xp'_*Yz~6РQF=~;qEa!1!aaX>jei:z4M$AAnUȅV%q2=,C9EeƸ } ~U?x[I'p}`$PEac\&䍛7oݺ?3;;xJ)Xԧ?}wb9x+/g3>yI'oAj\HEo?K/j_ .ᦛW_W#|cO:6n裏D+Qδ0J<6n4cGWS_^1D} ]l6AQ)9]c^j,Fq!t]5.9 Htκyϸu^vUߺ~,7qj3(cQs!1>ZZZI-̥.( _A}ڷRoq[Q\auÑ{:vH7@ynnPu|cZmJq}oqƭ=FKsk_ӻ1޼~>7󔧄a$Ry5|MvƟۿy?ɟrEzͅ7>#}-)wVلBZ}zulƍ?L}a9hXk՟nZo, 6W%֠o]_P,wȃ*ӷgͰVf^,r Y{{ m\SB?+5+ߩ}c@}W_ַmߜ W^ynx7w_|#.3>eoy;N=O/<’tЁtW|+_MS_|7?# [^wKE4Oꫵt<#{챟s3>^so{ټy _ǻk97۶ng?9?Oѭvce ' _馃O37r_ݰa#<p7>%t-/x |^v>眇m0x<{ 1Z,YD۸G/@4}]f5Y5``] >*v[oG/~ӟ{~;[k>?~'rO~?Gq|G o݋^bo\R~ֳ~a?{o?ٳ=/}GEΌ xO]xE駞\tQ^'8k^[%__=\s~K.ySnx1mTwU{ﳞ,8䓿g{$Iںn>Oso߼|}٧˼v[Wg0?ɟ|梋|䓄O>ׯo|ӛ9\{տۿj'/z~?<`L!2Y@+9xjA$ZV\^@;.9<#eƀL] zG +7}ES7_׿G`[m/~ o>t _6oF]qiWZع׽/|^6G?o|fӴ7@~wx(.w=oO [lټysEI).7477gk}g3ଳଳ*Sᣏrp/yKG'?zB'pM)/@ox54O3O<ƍi/+ýuѭS=I|A[l|yӋ_u!t~~Ї?|-ه?|G?Ж-]5A6oh4>{şԧ.'nܴ%jO>Y|np 7s1O<mϊEz<JZ\e,N,m=ĕ+=y_÷ApG~Ya^~np!ĵfYէB>y8ѳňj'nf8}<}zLL6Jr& +MUjb_౎#93 2 ` I3NBp!ZQ Enki9rﳊ c1B0 1cc,$B![ʰyݷZ-Ji2ϣ04ֺKiQAa"6v >Bֺ2vh4dwZ1BZƣr\1J@)5ouGqL )Iæ*93UJZy.v5t^AYQhm+(Z-Y(e=YL!}f[ R R$I,{ sC qgRq.0ZgYv2hNn»dNTZU9/TiBYBDIA81ZgYfa i1~d$^cf!,c !֌5G`(W׳ ˲7r-˃g%)Vj8#;e`NXc`i|vvv;gk eYXjIJrT.% )t$Izj(ws\(Ѡh2Zk看 p[Z/f2y26qPWt%TC1~9`b`:jE#BTqo9gDv] XkY;pR{ ?u f0+i *h)PODazP6ƴMyDR* c ^6%1Yf퍐qdBHaYL!J)XKzB;ys!z* 6,TJq]$7 lXݎ֢g\ʻ jFr ]FCS0;P ,HPBýn+PBakm~W>iB1#=k\-dc1iR(2mguM,WJaU?') MZf^l ԕZk>3O]{,Z}KI+ ,+Yxvgy5.x"aNY =oWL0 BC  1eQBℱB; ^K) \(/aw4PB\]C5.Ƙ wa w6cAL =+JZTn'&$/neY9Gy/m TiV3Y >a!ێʋ+R 1aj+{s.Dn+#t7n׍["`!]+rRJ5!.ڧ.Lc~ܸ-Y0~ p-M/=k BI3%-([-v̑ ( )Q( b1\pu\kLSiZ6$I0foi ]t#dJJ/J)J1Ɯ:ZcBggh*lƵiR^jY5ʥBc.PeIe7ѷݦ77R;tߧ e̗Z[xYK6DS4uk1M{"Ӧ=𘉰Ռ=Sf_ҚFcFh?UWgcl f#edT0/S+XSر ٣,p?:߳ @59)3H)5'x<РW(. =~kx<h09R犝& h=dƳ^TcwL J"$`А'hx&{<gtxV0^{<gJ߫YWrhĕ+=y,O8㧟8;o+ull@æ*hAhxZ8q֚)L qZ8TI0 Ehv@j[sW\N+ |2I"BfՔ 1QsΛ E 0L\0ѺKI/W !dɲl"Wx@!מaSqj*\QnXJhj5Bܜ윟wU@FQyuWo) 3B;Kz9,dz(0BDZ4M1RBD<Vv֠rVQ3c,J:cկvPaȅRJ)A̋#MӉL! W =YI)),M1J P Ydzb-8YkcyN_Anj, QqSƜ[:2iRq'p!(Ywah[WDn}Vqc1f~~^6`ce;D!pK<]3~v4M( M0 BFEQgYWTgZZmΝj?h<2Vc)z,;GyVcRjaaHꖇ]1S2*MUU*ZV\wDUu1)5=|uq4"(j3l{^A)jA M=g%[.PJRN.H)$ɲlvve.zD=c awSkaI'KeqeR* \EwE()#"V@)x<Z]ֵݡB(A`;>"xۭBℱB; ^K) \(/aw4PB\Ј.ƹeaÆ"=xĵZsA8nKƻpEG2rx_yaj2ϝhcJk(`u uj/"?,&T 4mLس1 ^% DZۊ@(-%ݍu(!Xu׊RkM)2eY- .'ݞȑ\R=fF[3xq:N9:[|^}ѧ|rF9k|.$ 0" ښ Mh*+MS11fIt$c̩C5&$y1Ynqq{+Mٌk57ҤR^ +{Uղ19XkK!(\Bvwq9G}߹sÆ oxӛ7^/^z kmoϪĂRhzx<+ g+s/;nfOO>m6w0IcL.,uo:/Kwםw>{%<gz)'߳L'A|D|$@yܒԂ 1}Bg3V|D}(NA`e6Y>#7_wɵ\s>җO=~? x<+B}\|*Ǒ@dȏV|)#)Qa d?Tx&޴+_!\.>?և~N?G?dz`˞~L&GN\ҀV(f ' _ÂҰ:elsRwl{=k9c" o%bnBPss h@!EZUPX5yQ&QAx[n{ku{͞5cݫ{|^{s~ko]}Y{{>yxN_ ցM Nw̆7s`fpxKv- 3k?7O5sicoޙ[0P]~Jk~l&-\ vݖȟ~??]/x^ٿ^җbw~=yK.>P߿/|?iOÎ~{zʕzA7; nv@c~O^]| x׻Oh/'>j\ cx{Hsi-kAw:R;܊ nHA-MS$޹{I5FڄV~0u=`}iݝ^O 1l^u+^bW98s؎g%IiY/WeR)ܘKoj$Jc-}Qg`m0ct"^ONVkʢ ΝVByzt$c,k~ݧߡ^*w2?gLpcdAgZwEz45@NvK$iF,Gs4Y 1ZkG}gw.u)FBlj97xb])k./))ѕ$n-tlLYg-ώ;媪KWeYFUfcX[ ;kwR bl֌1!D IDATAU76Y%vrA>~y_s."XeeIe)ccYU5/v[k{{J q}ёm*zJJpuE<,JyREChY)4#Fh4 sQw >8vJJVIB}s LL)լ)mlF+^/II YD ]\R:P.Xkjn390^)8_$7,SZUSZcUUyQf] `V9<<W'֢k;1d4cUY !LEI{﫪 !(LD(UU)1{h$IkR^'n l?A!a2ƚc nX+ ~Zcl7jc,C_&㼞 F!H!0@.ƥZR pՔopmpVVg - !,](!()!8&K Co[MGW:甔Iҙn;ֵ6&sLERrN f㼟ZýGmUUIcpYhLc~Yk1g oB5ػAq={~sffiM J8Ơf dSR_iz@1Z,ֺ?gȇ?X37{'~W׽=uG)vhwnwN){υg]UJkp{oy+ttp8{=eܬ׿F@+C+.,ӬBuUf&'uU5=`-!e{o)%LNkkMCyú͐JIAArnfElT]atkuӕ6Ͼ7y܃}+_ y+^>{+,F=y΋;׼_GßW_y]w*Zb|͢(oQ갭vg CAu[f9o/}| UJ_[k}s۴o^/k'?~Ujcϟ?H :p/}iݶw?|eAlcK"?Mfrxq{|ӟ#|3?{tg? .}Vk/>Oyʧ>)xG>?u~w5wo}?oy} g~9y_Ї'›|Y~6g?~wu]?0?_~)O}[}#]zի_K%  nvX|跼mk߿9?C,D>汏~'}/zѯگwV C?w(.^~ oxՏ㓷G=Jg?G_=og*/{vǣfw{k~'7}qYӞ/B_>?cwqG^xo{7-oWsܗ6]m   nOvU}ėozӟɟ_5!<>1yLe}/O|7~7q~.\tާ=i-_G?z~W,w/?O: &kmbx|?}?˿<88BZ{Ec weY'=S?pg./C??[η.\?cL|S㓟,޻ΟVO3'? ϸt|Әmyp—.]zS,0ƞg?L^  bvU_>5キq.\xғ.]G=ػ?~}G}_җbc<)'Ʈ|?eO?Dwu׋_<@s$Ky?pŃF㼢,s=/yɹs6H&8=A"oCl=c(Yy窲͏|),/~Dozw4MGGG>S?z_~>/~ߜP7"_3Rޅ~wo}k{]/xAn߼|37mOӀu{я|䡋o?s}WgunyFNuu]VkEA-MS$޹{I5 !,a0u=`}ia!Ɵ|kQ罞b4aMЪ{S_Q$o[?7G?ۿ[\"s)$IeQK"`倫2cN|c,?|$QJM߼æzr*\Smpx||u*z$Wn5ƤY6~+8Zdcl0w׭ʝ#\p9cX#OU Xn Ic ngg,CRJ!VW'g,ĸl˹s;^fU(F#%eh6k~֯j1iʪ!lwh0c\[Ƭn)Һ*k3I&muZ7eRc3-s3Z!R*AA79פ@ZR*1k5k\)UUՊ5}11TjS9T1C-F#Bm6`e҂0`p|;rcZ9*ёKgss9c/rۡc,/|a?|͌vx4˒$)2MSX 3Ʋ,j_:ZO~;#)e7y5}y\G's$]d΂nTUzS;4M+m1Ir)}rn  $I )di nX+ ~Zcl7jc,C_&㼞 !H!0@.ƥZR pՔopml]$jBCPR:q5Cp^M-K Co[MGW:甔Iҙn;ֵ6/cLH;v7u5)7y?"g !ڎ$ ;hLc~pܶO9fxBh#nJ rAqݐ{~sffiM J8Ơf !4r)qzj^PmK.EQ$ieYpc4xƖn1U2u]`wN){υg]UJkp{rGWüÑfRvV(hcў.#ӬBuUf&'uU5=`-!e{o)%LNkkMCyú͐JI}t!D&QlEI3 ͼj< p8| 9 K @A $  ⶀ?AAAl'Aq9MOfv ַҭ xEzOg$ ޞ9cH!zM3eAABP­AYF w3^ f$vcɡu7RfY gc63's˗g4>c:b0p[aъ n+w~yݒ!2\l{q׺(j<ٸlcCY@m5n45lv5{$I4e9sB3bzC6L14r΍1޹n %ֺ9X)1~,˭Low sUUʲ<1fi/3Ƅ!)W]6Y%I$Is8OӔu,"`cp"hj30s~0P$Z9F00sE׾> K Uͮ@!al&j!T$8+uq`FBGlo]*D(H>"BG%uYj>? X{B]ڐo= ڢ(ߟ W+狂@,SZUSZcUUyQf ! :]9@kݨkƪB$)$A9WUBPJ$eQ1~5RU):ZyQ(de8!ٞE ι+/c0X9!F#'*Knl4ˤeY|a*2u]Z!D$i3揨Mq)4zݽ!TUz鿺=!~+xz<=~+6RJ=NHCR)m 'cEQdcGFe0PJ9!@J{lʥ{}!9ir!b}IxkaJEl_t1E!TZZntfp!k4^ӣ~r4'd| x @ɜ}-3Pk E,r`jp  'Nr!B1OYF@*Ol5R; Fd) b9DW\F_Z!Ư hԒ1\7Z+AEQ ]e9IC\ С2}Жctuu]60-gW\9.D$W ň]0! 1K/\9j拺{16AJ9k_qp$TU%n/cȄv4IkRmDڪ9\?1%`3Hp$,*ykqXV~:BK/tKӰu`0H؄WG8bYGOuѵ 6\>ǩl`E望 u?X>B" '8a[Du $$ICh/3ƚcƘZ- 〄~o1Zk1Y/q^Of tJkK A)KYM5cia)BԫhUU$1+]+%W3;'2 mvAGWbp$>tn0l]kc9fgvB5ػ;ڑuU'A=[oNc0yFk_%7{WZsz(M^/09B33KUUYy^51.͊fnx?G/qTEapѼ'ZfScrSq|8a,j}u.X?v ';03tda[:c5t, cqAUiNSZkcbL/4^d-^k]H4˲ƚi-s񸭍f,8"r> ޹f =uU)UCq-r]9^Guna|s[FRZC!F{cL>䤮t2Fbn/K)ar^[kw3umTJ*E%n?غ"S,M\9Ƙ&Ŀ,K8k,Z#D{@VU 'kgy1'8WZ<+wc*.V^/DW@E|ND5nr8sw/,"ꬥˍ:l#9h䧇 CA0H}ppmX/w_: N ̀DL$ pdL^lD{/81ބK0FW:͵0cLq[↰ɷ{CXK?^ۜ7~!AA9&4iP$I duޭE_6ɾE la $ Dvm',p7'dMu?Ln{l|-On] 7ݤ.t.Bҟ XvŸ&Ʋ]ݿ+(>u?֍!V?A {o5]8rùͦAĩ OÐן X AǒCnȀկX]7*k3o~q %m$$IChKIƘֺym怵|a1vCk6&2e2Nb\zu!(0KYMx w9$I1]wTa&(^!ˇtj༚ ūY!mc{ˋJ眒2IB:m|s[ٺyBJi\"BpcS+rZ;60ڧ.9cz ZG  n7k圧Y@eSs|a,1Bh RZkcbL/4^d-^k]H4˲ƚi-s)XDIHVFf,8k\sJ)T{.D=#RZ~?pت[^DGWüÑfwV(hcў.#ӬBuUf&'uU5PdؐR䤽$ ;Lg8 TJz^@'U7چUy3 @d cY`A,g!}iW> b Tė ր?AI' :'Oې' h v% n$ R7 bsHAz9 &Dh%݀Xa鱺GX͌\i}?ͽ~;::N 0ct"[v81A;IO$wիI5 !T+JӴ^LIp8Ěk%5~0Xr=aZJ$IӲ(JGGXT1iEXX|A!za|ǰhEz הE;1c ^'8<<9r}+N *u[#4>sOʟ b7Y(45D3*N}Y!)YsBvRFJ4M8\YJ)ֺ( *pp:+W@)C)Sg^ 16˶DnԒ1\ۃnJɳAEQ|tSCˌEZc$\݉W Xuk9|Y1)`:ˢ1eQ载drcc:*{۵a-g-GA)RIYH)T?H!+^`xR131 jFzЏ޸xWi5[אJ)!uN cN0u1)#1NSE?k҇6f*Eab髀 bWBc ^NjVI?0m)tkFIal |'{TL<\sI&Iү~96Ĉ0(z^q{V-|g +c}.5f8 sc8 QUUM>w`y$cҤO#,dA Iz1n^c0D9`4_$}kmZɲ }z23 S^k]bJ) RVS1]êRɅWª*Ic z7!()!8&sCia~HfttsNI$!I6l]kcc:b0p[aъ n" Kij;w_$냆iiZ_&$ cl8bMٵkcLeeQp!c;.$IeQe9 sqc0V/=_Pit1l:Zm^''5eAXB !;M~+8Zdcl0wm ~ fO[ͨ8eYByA{S׵Zk]'rR#Αiq΍1޹n_FZ,OErUUZ)Һ* zL6Bn+f8kBT dzA;5)V0ƨJZMz[S}ёm*zJJpOY<,JyRt9l:Z@e ͈97傹#I8'%xkt 0IUYYZe>mF+^/IIB'a)s債(6`=J) ,˔uU9Ƙ}UU{YF%z,pxx(CN*EjZ7ڴ,&Iʢ$IP~qνUUR&I"@Ys_ }ᤔs7~nUU)1{T]׫Z7hBYr2MY}˜h4RJ)q6O+,Bߜo9l:ZeYIY?69Jtv PRJTkť0\ AAăW-)?+2!|1^rR92u]~uYzclJ IDATKF-Ykc8qε=Z< “Z6(~yqĺZ$ƹoGlp8ĵ}V7.z]㠦B? nlI dP͢1ֹ)Dǰhx{1?+c;AAZ82ޯӀ !pn1BsXXz@& /Y)k:kZ'uyri1[p?eDlmg|BcM?:綕21&3_gY46, L?t c 0V5XRp}oVAH%e gP4"#H{=I1h5RV:`Ɩp{49i$*Mv`嫃B_Jʢz!)5Ru9+c}.5f8 sc8 QUUM>w`y$)cH)7J%i$$IChLƘֺym怵|a1vCk6&2e2Nb\zu!(0KYMx wY$1Ơw3sX =%|JJ=fΫP4,2i۹-o@GW:甔Iҙn;ֵ6&sLERrN f㼟Zzma>u ̙nms;!:6Aq!X.<2|, cqB#NZCgJ}z&kZREYE7LglVJRcGx1n͹.9C=uU)UCq ny:r84ܬ׿F@+CtƘf} 7j439"s&'&Ip!|g86̻/?Ar'cHkM1ι`Lciz<Apҗv# V| vAJ}?Aq' VyEA$ U8QC' ւ?AĮBҟ X !]6`9Xrh z4uK<ϕW._[d_Y;l ;MG+  Kij;w_$냆iiZ_&$ cl8bMٵkzx|l$IҴ,,,Jq PTEվc1R{{rڬ'w V[ɪpMYs1zBC\=:ª`I rߊSEf0|JzƐߟ bWY(45D3JX}Y!)b+g^g,h'k2MS4snΡRzk@k$IhX^_IjcZ3jcʲa|ǰ>\UVJ2Ƃ1$ݤkֺ(N,3_rG;NR(jjz3.Q$EQqdfd7=Y %p8FDu}1Z[Va4" OΫYokkmuژ{DCǑX}.w4UUe ~ ȍe CJ{_e"JRJ0xy* rW%m۲h۶,KD[x-BXYksKvMZ7sn^oCZӓ1f^7%UW+k _)-R)Uյ¡$d<>63 Od3cMӌgZ>)Þ1i>Zx/ ڶ*k<+1ժ() \0\!v6d9zZJ]9u{O)u]R2JoG2Pi]*k97WصE@QY~)RJ]u{R?ul?gK)+VJkDtڳY@۷\4jsMXksyx3oLteYUF<d<>63C/ O[1Zk˲MI\-.Ӆٗ.Riӈd*>3teSE"*O?Tu]nyfz󓟊iyyZ;n* 06AaRJJk(hlp2_ā>(dj>':g䨯iVtoZ֭v,b8h#^sk- NS&m[uZW/1Zk:L))wuήL*0ӕ^rCO3p6U4 ଵ31/9~ ]ל N{<L1ᠽEw(svZcBl w_?چoA̟ɜRJ҈LǟnA_ϐ+  PAHA:(_#_A AA  [ _A +1?ڀ#n݀SUU>zf6~꺶=|vzl5">==}'&lc0cc3J~y@A/̜/E%A[|VYKN:-m>Sfs?9}y YEQevmVUeU@D}zz)%u df&Zph///j~||c秧|4x˪ʏM~g^.[r —/{$M)mKD| H ڟ]Xcʲާ3Ƥ9WMX_L۶2!?|*w]B0X纶%&;*"//$}[s.cqhZ3b!^hDA>gAAJl-zF꺶v]7yL<{IϺD}.w4UUe ~ ȍe CJ{_e"JRJ0xy* rW%m۲h۶,KD[x-BXYksKL xz1:9u/rZYc r_OoyܕJ%%aIUUlsiq5_q1pS5&5M‡^kRE]ۖUe]27q1fZEq7_P %G*11,Bݮfs6Y^蹷֢RfUUֹb9=u]J)ˬ>+ѿ CuܨJ_e4b׶Zk_nEQdJ)u]GDZ_ vg_FuZ}J)kq_gS]N{6o1>|7\RZfޒʲy~)?wx}lfRUU>{(jBVbڲ,74Jc *'7kL"꺮(KNH} ? dA*o90X?+&]`M1f)fY\-GTsj)ycNz2hۭ뺮Ҙ( &jץTgI)e߼sP{vs#k/LWZc%ED3} aPͥ'̈́O˃iOx☟0VQo({ |]L:x"RZ3DlιIhԧ)eu{)ϾduYJk!O)݅ZkmbιɭrsYf\4p/u~1~^`1S.=Qo]q8=y(Į XZ>*%_b1m^ ij Qd,!VVÓ1#UMӄ[MBCYZk1!F53OդɃgE.(8 2Sy"eggm=p?Zx _Kc{Tŋ22 r(` GAu 9J*ǖ({?nM\^غ]ߔ* }c j"#uV{^ʔ0ƘScģTymVA|7W#cEYEO$oh/ǤchA!|)DD{CnZ C@z!04ay_UUeRa0:s;]W˺& |Ց.1>ڮS,"ʡ35jDd) Tw$*0ӕ1FkLQ==}'&lc0cc3JAK0'˲Eb|~~~I(s|P"Ue1I(q3eo6sۗȭ(lwm2*D NQ_{噷//RRrz[}OfylfR}  7vZVZ<&y~z{_VU~l;:ܷpt DwH/AAr\EYzDs)m/걵?Ɣe9O1fgI)1s( |eY&裚mۣbT1:׵-8 E|y&:vk-0CkDZ݇C!hjF#  Jl-zF꺶v]7yL<{y6""!뻻|yTU=3l6T5 #7Ζ5& !s+ R1FR* $S:3}K0s۶eUEѶmY84Z&斜/=MZ~ GkdYͩxI1ƗCz|TJUump(I/L*f3cMӌgZ>)O deMZ{|Q@׶eUYka\aYVEQ\M [y Ƙc !nuf9,C/t![kQKA UUY1ZRu),D~x||,C$沈s*}ш]j}QER*uYk}Q0@۝1~]Yk)SFsR!ƻ;3C&÷o9hz*+iku.o-i,hc~f&UUUι(+?Qo%h-ySJ7ƠRqq$늲t}TX' Q=ZT7bM8geD),el6se远jQ-1{bIּ[vz]51_EQ0Q{֘Bu=3դo9bWl<7vRt5& QRD4:?\\zLt<qΝFwQ/Y3k% ds'r*)>#EMDvۈH}Z]P /Yk]:D6RJ}; -\׵z4t, ]9#Ry'aZrpyꌟR_J5 c*hs?|=f٣rXlx{1JA/ƴy 駩~F=sHW+I\ǘI4k^e4MX ĉ#Z }eiƄeuGs{íb.r(LEQuޫQi$+YcbJy6C+ j>d*>3teSE"*O?TyQ߳M^uGkFk}LhF.RZO]E7tk}y4'?K3v1UA~7LJ)3}W)UVU~eU nZ 8pe,QYLԟ5j؝mrW˺nWeUU txī{ciwq܄Rv\"n )FkmV)%u"8^fr֫UnΦj9`&2gޏcyC뚓Iucg[ژ>8!wn7Xk#~+xt_+mR7}? BODdNI!*cXXhA +  oHAaYR" IDAT G 0 P}A<"A%_#_AB> ? 5%Gԡϣ?7u]?ϳP2,? fZY(R/w.JDʲwLrRJ>P_I ?Qͭ(,SJm1Fǣm៓BA~IL3~{ KxU];kZ[xl]v%x)yůXLԇkZ {=@SUU4eR1FR*Ϟ̱Cy恙7Ƭ۷i1ƗUVJUump⧯*3^Vn7iXFDmێ| L W5c̢0fDR zԩTUe }l11?÷ow]b׶]ko6 lvJim/,s÷o֚۶5jEYUFmJe*;:kmYUB pk)3Z窲dk1Ʊk.%p; IbA,&;Jo90X_i ibPJm6`M1fMfY%B Ḍt"`5F/̜-l{oj5&ĘD4::*_bιKr =AtRe,A@L0AE9yξJʪ9giW4娕pВZ,*q6GM]gZ"1&ֈh?MuNQJQJ/KV?j 0֘ysY~f gSVu]J?d[Æn[Rn|Y "_Kc{Tŋ2 rͯSO5/BrLQ1m0!3V!+YB vY HiÄ7_-K6E |U EDS0":xM|뺢,u Cb9T?n'"u:U7DڔG?K䲪r{sSE"*wu"hckPuN뺪VUlK~%p+AcRJژp(1R>0PVՑ i/dG( vBD+bZʲ\v]QUU1@??$_9Qwrt׌~-n1n?,+f3z?ϯ9vG6}櫪2-M4TUUmU!AN00Uay Q:03(|`@F ̜3 gm@EGc . 6)bfj zUkZ጑cRlc,_x ?1N,,v٧s?b/5kЍy>0Zݿ}S+>N^ݏs73D\īYﭵgumnLy)yůXLԇkZ {=@SUU]^1c|ϖ}[Fk'lNclfcJeevכl6!Tn+"zzzcj<|^3δLSf`7'-w(/y!oD'N?Rǟw=赬K̂?ƘcnnJ)@\rԩtTUe }l11?÷ow] Zk"z||lXb,(hݶm4ZRUs.n֖UČLQPVt_o++B~3[7?-k&6_锳fL1|&G͞bJY")A653*DDTJi5qs>؛<"+ݟyC=.UGj>u53ddӍ" 'c#>EJw.go@퍆#A&q9L9tZV_v04qx\h*Ro>i`rRn`~xxkm1K7/CE۶{un,kL1ohyu."fnιc(·Cp]7!~l=-k&6_%bf9pEj_8&_cl,*V~4x&?Թ-'*NpsJ_9a}|3 GvTn:O!!s;{2?]0L#ytљǙҌ$k5?1;}Ó@+!s? N?8A{D㊹ .- M (C)zx}Q>z9 yO#hќjGZtɀ`5qNg? GwEv3|by4KP85|(8F>]RfݿwʹaG3.ڕ;{J蕛\GAJ):胖d]*+cط1f,1+}5By}iZi̸6N1,!~\ Œ#`|6 ƍ;BrX?HA)~F_7ey} > Džs׫q $a̼_N~|LEӍO"O4HC!135<} '>k?Jкicm&O{믫7$oz?ߓ͆^> ߶,A?"'a~J)ZBan}b mMA*!}oؤRX翾o߼,A~n8v(~A/HO!Ÿ]w.e 58l1RLϮA~&D W8R̔u?ٓMAYyY @^̆BZA|q W 췱I)- A1}D |9׾9G7DA[[/Gl;cC;9kƌ~iM '19H _ҿ(KGm\EQ)mAO_* ԼJl-Z{6Y]ڮvݒbLw)8>]a I p@ |]GF.]7]#0%x/ `\ r/\⏾ |oFSAPJ}A0"A+ A#_ dq ¯HAAA-/ R8 Jm? A~kdA:6P j`Nv ~ |IB8ڡ^#g>lk%7oQEY]۶u][fN38_^^bZ֏9v%e"xAi Ś8Fůct+/p vmʩ=1h_@Cѝ#ؠ @@ *0]^+si_/Tu mL- C/1O@'P@Y`NС縃4^)^1kPR? ;0u][k[x:zDDpeP{}}{_e"JRJ۾*k-"2siL]m }Q@׶eUYkgm˪*m۲,qaf1c;9ƘjU˹km}% p4 Wށ`ǡ#٫-AX2 dNp %\# CXel4[?2tIBFݯTIK#* qҿVZ!>pTuj>U7bfRQܡ*:楳g8-pTLg&TuL/DY ;NOhA pP;NCJo@[GHyR dc۶,s^)nnC(r8kۮ댵}߷m>TU#묵euq Rfuw5&u]ι1au1オZ K@{H-hnk U/,T] |8{a_׺.=jt 1A4?1h I ^K$gh_{w`Nar Gx;T[T =s$zdn)}w{^f{sz IDATx85 4,8o=p?Sh=:@BR Jۀ?`ֲ[ ܊i9 aM۶s=`5;zC1F\KZk{TsH"1jDJ  Ũsvkjq  xIzA87 BC3P7kl@4[x2 4M<\B8%"(DO7 T gRISsTϐZ5`tUbzAU; QI/̖9tء. mU A| +0%p$2A7ayZ,*84&KjD4߰[s}UUZk`Σ = v}!ZcO)}z>Chw;wwW WYVpuN< |)Ju!]8lg!$n,b Q#vdQNAw =8rL2)@N;Ty;&PV>J P Lʁ2A\[Vu]J뺾$>YKD^ShfHDyhP4em[uXߔA,1ૻ!x o6Pۙ4 іPQb8BxIC`q_{@|aWX*(z4OgS]/riReTn0G]SÔe/ R|RfClwZPR3-"ʃ@E ܼwT׶y0Cq;7ژ'8EanYp WK^Ƙio 02 I$hJS`ҬX;OwLk, e*#m@ЯW42߅"n5;DaQXj'{J;Dj=@Պhp-JӪVj)*ǩ%z\Xku ^ LSJWQQ9@;NuTsA{' F q* ĭ#a%Z{y_u^QWw!"mL.+daΙN#h}Q\"j[ι%RJZsZk,Et\FWՄ 3")FTl )% `XC>kFț{G}Ex_bįE~xE'~ޠ`: I E%~b(w Z{W#9VLXhkNEZ 03qWK6k}NπV隸?gi:T53b Q/]CAx7oY{nWeUU C %;T6MZ0sXtYKSJ {1p|Q8Hͅx]auGɍڛRJ @YUL$_ C2OӢ<\r _x ~J=ȑs}':*V=Dˤ?}x(hsOJ 69(p^sK ~nJ {AG۰_G}8P0NDD*u1GJ5J (0|EhV[f | ߙ1G i@S^5?+J w@  pJsjJj&p9OAXUA/ \a)_ —F p "A A{u} A~yeA/҂s(oP. |=۶m}?.CA_ q |YL]ڮvRU] 8ȫ""3arkc-"s"z~z=y_x"j۶|iلRcVu19C뻻D=]4|S㙨a4Ӳ1Ɨ%(u?(_bRx)攲mQQߪs}߇EQUlPukRX;UeRvl929ԥ!7c߾eHu\YeYvR}73VUe.hSJ]׍e1>>>zEɼ ">&v o4MU1!CDsY[(RJgͶqM;?էmLfDZa.Vʛcw0-( Yk `Z?6tyeK>e漜sbcD*)Ja/ q)`+B&+(%^r@}YE]6ERtF~).J"[w-*½"J-@Wo+Kv G-EaSd}LH*SAJJrsNcL)}6BB#O1;JR)" bܮ0&?E0̧HgAز=#tG"ַ x"8Z"JkJϋ񢝣茢 44Qw7;}8>j("a|7@RJiWUTح~3 PLIRcO(hƱuEc%'S㍟(b\c"WJ uHr p[ueJIH#/m}>DMBK!Ďa|`D$Gf}q 0 1%x%a$\BJES*#bsں(M,K"xODȳl;DJ)":)Oʪʑgu]}wnSJJJTJSzUU0_q /0?@Dїarqzg( 'ϛ!68Vh?9DVe Łs!Rz~~;3(lCrω ADmRr}~w1 |b|a,^ڷMs魲iNәy= q*NJ9Za“( 10 59+Za k@@xfa?0 sӟa+s䞟 WaR@(_a [a) & 0_VaS%$2 |e0 Q[0 5U_,q˺1~ xzzv>o;NXk`ޛrwo}`wC/ |Ta?(]u?ҌvŽeZ[VU׶ev,n*u^O)9rq\Qk y,mQ*xֺkKl ! W}2TUADJaUUFD ")-Y 7φIk0 s.Wwz81ƘAZѧ+b.0E'4xwRjlZ}6(Kk-9ZURX[>nXpRڲ,cJ]SJr8vEYk{w))%"Zۢ ||Sf>i-XaKZ}枾B>l>ϟǞ6vFH!BB!\#!"H))([-Ze%QLuc=֌(% !ɺ1F(mz`M:眔r4X ͩ;eX,{!DnOJ)۹_F'轟?=i}6l6+bQ8zZ1+*uasNkS$)e{ԫk(`M]1y|]7N֯^_OhRjf6. cLh$}\.aySf)A/`?0̗@!|^YUJʮ뚦B(CA[;bu}+iPU1&ZU5'L)=??g=E( mjնmJfd\^KAYJsd]Gqp 1|ʒڶm&+1@>Sc Vj7wiО9B)%=x.QD6*ơ#VEE1)6lQ T>"\.E5WBk7Z%w=co洷q!Xnà 9cLqo#'Z| X)`?C*R]tB)|s0\c"1SOV1rv?BqZq%rf#OʺBQEQ9Oڇ.uדzRjCi,Yƒd?$9;MhT"]}~c׳Y](ѿ)f^__{Sf10ׅI Bbz_TU$Cω40*"Tnaw~eUeI=;g:HMʲD!DUwk1(;#IYW}QbE}UU٬wR;*)%%R*k̺uUϮb N) hRc- /ҺmRkmэ`.07o=jsDbJecTJI)(9g:ڷ{lQ䋒]wsfzU$}b܏)s(z6K9L5y1/)3?|_?DNn BBn7B|\^٬( =̨!=m4`Q"03Vh?]$sM 6XK)?OʺT*lW5ιUe !̻]wV\몮///(E"hX"&Ob:_ @D!a{!/ȻeZa(ZkuI)h== \..|'"OUaaC cG ~~Y,}鞞)Fvfۃ1vA%Ib2}'i9#b*J#[7z/ RyS J{\ݺ?=s&?ϑ 䄮;O)B& !["g+;3vaa""&t#+I1{1'I b8[y,kulHd-2$!!~c !s4TUB6JkDBhfP"[u],+%}I.ZI >ޫwl!0M63?*.4 0eYM)|+-JRs@NJik[("Bc)%- 趵#K)(9d] Ƹ!hc)ƾ*%eu)a@ R16pɿw,_73G1goD "Cal+Md;z>JJխZYιb1Vʇufaq$`0+16&/Y;P1s\}d,K!&sD/c7RJDTJ-N"b5)ݓ6 ! TBa@Bi M7z6CcDTJEB!F#w9IY}2 |bR^e?x/Hk;x@R5v|N#.!]m+UU崘!$"k"oR#!DJ b@$6IJ)4 Ε0rVBZ63?cxϸT$="$)ߗ|C)i?6&1)몪:{{猵u]g|@ア6Ơ{KBJES*bS-1*j=a<}zKsG̷\)q`J cd15ah۶(˪ xVJcCi0Zkc(wsi1ZJp͊s4i9¶ms@ ޿Ӽa棹r˜V|1灌 9-W4WkޠsYoѕǾ~>4Mv_0ZKj^s>^&:BVdrJ "ƀ#/$ 0Е6D' u^|88a9+ 0Va y"0 Ua՟a98@a 3 0'!a4lgaab.Maa~(տ`՟ay$R0 ü+3 0+ 10mJHT4D5wjYPmiYCPU׵ֺ3BTuNi]JkDf|R Bo(B4DI0 ]z! ?1 s9ƒ6`#c$U* (,}d qd,}ރU&wE !QsW/JIu]J( ĿYުƸ!hc)ƾs҅l-Eu(cs{/,f2 0WBi?^AX!ܡ:y B-Hicľþ+GQ*,if5Ƅc7{(BĮǮRPadJ |Ab#PMc$Z)BuR!4Mιb1rQuZ%e.ƄQY׊S2 |&#^DGQ(1oPDj]DQDJu!-XBiZ0&e*L sTd-(_x41ẃ5թPX w)- [Z%vW[2|".M7@t)% Pa<_DĘgJI4*?"Rj\^} IDAT 0;smRZIQ9c?3Ex+z't@$S=;t^kJRbJn?;HJy1Нk?0`Iйwrơܮ9E8VGl_sd.0 ]ASeEhP| 4RquhZ՗AmɺY!IEk!>p kM%/QBUPX2:MPw4$`mSiFA|_W4Ȳ. 1(`ODrdBt !"|0 s "@$f)lP1#!QH^!KJ:t A$QG!bߟp?FcoB*H՟Dl4+H _})@!VN0V(RX{o*;9cm]{DJ qaC!(x71K8ae)1RB6a3)i -T@ ̉ $cdݛ|u=E9 4 "jcsm JDa~#Gy(ؽ gԷ(u(KB ?17#cQ*QxS[SoAl Vm׼5ei{S%tdu]7=Em͔;>v)b]e( cL.J)wzic'{D住1t&"~F}K(%/ 1v-Qq71Qy?F!F[Up ՔD.uiV׵ֺwZim|>D R6sVUQDQRִBx|Ti|kML:v^ӲQkBlֵ`oe- "UJRۺ߿< 'pz]}Q&AD}߿lVE7q ~Φw!eL)_:,K\x|._^^rq1kz6Ӈі,(ެ ٿ9׬p7ч'Q̗(( 63ѝD=Æ1T[M _)BȺmeb0!R2PU6;׬!b)eJm[u1WUeɓ]# pTuJk*JIu]4Rs3&d; Y-<(ݨާjx\'+ ثjZy!syMT8݇Ǯ-`3ƄRJbsS2B= s&RSJw"Ϡ|~"Bh{.0=??;8Tlz}xf2_lg7;=-Q>4+ӯ}[4N0?kQMn7*ilZ !AV41f7c1v-D-}cy1A6GyySQ.Z%JI54XCOrT5s0ƄH#NVsl\!+ IuZD}xzpQ9/"t( ژ`^f9wra-ƘjE$pz&"Ňi& 0eҬN۫ﱙ|kE`ہ.EC0)Q+Z4=|q=^y\O[[ ^gV-m19wD#ߔiH])%"*HJvl,m7\t76b|/ჲ&kBD ϡ,KDF:ղ7N d턫&*<ه-󚦮k)iQiB0H)Rcm^C1xw+H%}n JrFG`א1dKl^ۖ4`K|uCZA7Yi%>m0/S]pRa= ?hn/P6ߎJu9;2Ҫ9W)IG}2nla6iMcL1N d \MگW 9!rL4~$TkCȺsN>=cUZ*CPRQJ[8E(nb+_ҽ A!QjP xBQ-SGpUԆ䟽O}ZBK8B!zbFM\1RI5QohpVJ 5“4"{aJ'L (ί!G~DbLfE_)#fDFq oϛ_?-7ۖ~#c,4_LL[*9GY}g Dplغ oc Z]~\7VUBƠn!uRJJ9x{猵u]{]v64%V(Ry|^5!k3_-{kNIrmeUKVR*]2sݰÓ()(-0gBic?Nt "!buW@|_bR)B M0ߦiQc\ߏʜ܏!6s"wJD٭z6˹J|'kBD '(4ZVJNIB1z>eMp]ZiMqΜB^˪7gb9/*ݻt{~g/b6Nr;rݰÓ(?y_ˆ~4z#{{߯ -8t~ssz<-)C6f9ɿwMÒQ$]>]n:?3TZ4u-2ϢWf> C $Hb! ,ace`woOڟ>ϻª >e1 0_k1̇\?^eOaiD-l2_F?aaaê?0 0 XgaaŪׯ_OOOѠ}vUׯȺr63,< b\u}{=Qa#0 Ì87'ߡ=(a;%{>)E a'r:ϝr֫xgm1eYvd>K5lRJ/-lQXcdo8ZN,u]~ku4(n2TBD0 I ?OW)%jRv?ϔ,˘R1t^*"ڿz樢(1v]RRJweysmwNim_QV.RƘRJp>'Qw0 ||>``ۉ9׶nPU:!iOOR6T !yJUJ90kMUUF=X!s@RBXLzu]+bJ]R<7@GQJANP5Qֺ9)֦=;8猵6dY4uQJEᝫ2[R8Bk=[gm~g"QbXI뺡I'uJa["h^iczVc۶{o([TUeIZ벪ڦRfU]@Zb~7/ȱŒ2uE( mjնmTBwH*[:ĸ{p"gmj"\.E5ZѶm+ !ڳcU캮i)؟`ާ-T!dbDic$|B0)/'8 qݣa~8TWiH'ouiGh P$ ʇlM) FYla+Bvjʊ<= ^W7g=*'@Y^*}MZ'cDہ "pp{c$Y'%hfPtQް4`6+fpӌpZ;blTRʲ,"۷ uRJD̩im[)e6@V:6)Ƙ w2"Q)q>ee[I鷐R )}?I>(qRjkc̍+ Ӳ" RbLBC`Xkk[c rGboɁ=AJ{<6>ظQ0?:r&'썙r}!zK3c/%|fRUBHlvScejsCJDx3v5(R(:،:{K࿞:9uGc1*ȯZwX[׵R1;enZbt#c^JII7DeB(,*{?ڪB{Ra!r*D6BDkm9[RB}C1/ҍƁ΃{"?r(EQ SM(˲ū;*"Tn;NRo[=8j6Rjn` ^z/O c㓍[.%|-0%( $#meYUT/uh JD9Z[ksYm6g! ZHEQf3,cNCDZ)u܏eB1z>,Dk)%يFYWP,"CF/s;i c u, ˜KHa4y?5DN{euJ^"\Esl#]Pq\zmi6)ڶus!? ""_EQ&b"s4eyrO(kN!Ux`BֺkzTumnk˜ zGƢT]A>H4kDZߪ% Z%"v3m&BEQec.:A 8x7Zk5p@G'AY{1FSZD'R)cLu)%v=g![>]KPY\igVnetY*EyS׵)%};20/ B)^b-2$!!lO|ߦC#"-5th!Ŕ^G UUxB4g.hҨ;ڭgti#rY)bӍ)NRj6c;R !V+RΟk9UUc`Pq qWlιV1G\4qW^q'NYQյ^(n]9q/_wO5#e5M*;We"CwΔB@f\16fBH?OYdp̖~Pf&iuO65WcUU!m68~QcNki]'E ZEQt#]Xim6o;!DRRP6<8"ޯVG)eOEn?ΩZk@L)Zic _Z]wYZK*s3ݽu]gURfN1f}{X.l 6zO {cֺvؿ&)k|+4,MM5'Og>y'C_HHd v-HJ BtF[,P:#"Ŀ osaÛ􈶈H \% XB:C [ub. akF1 ?sX[B8 O) T#SPJYRU91OOO1u )%"*#5Lvo=Rjkc̙6t]WX[jۣ1x?ﭵ8/֞iB}{i;(Dol9"hڱL&SRІlj(h:v/O]]4|LʧDRsVMɼw) oY*+@ *+$( F*-y' IDATMQ=e a›ߍt#("Bx¹DJ E@ٓg 8W@VܖH=!@1ʿn%RZjBȺs:־01ӷd] DcmQhZ"a42٢ӂlu]߫M.d]w:L_cDaO2U]#b4UUUUuc> zO t=7C6+EJ P/hb7SR5vݾ݁aA0;}? 9"R"8&8=-B%k!M!;ǫ&TkB!TgO⽧=!}EQ䖟`۶['uߔrvHD(lu^nB!Yf\"kgWLwԱ qCNJyM3qWpc@$6JJ)8he !!MZeyav1I֞S! >P$q[IHȉ3mpXo1ZJmy'b (+K۶EYVUE{-Vh?tuMZDȟeU{#Q^BEU%[tu]g)bZ-XkKQ)!5&5R7-]ڇ_[4tsz瀵Rzݬ}PV4J(sUY&i``J! x~3"Ο(R1|@s>\w%?= k}sjǻlqxqx݈Msm[856_0vZr\ :Wi}׵XY(VarXCSJB[eӎ0"E)﫣SY"~ny7?fݷP޹6Zb1 owE6 E5 1eU))I2~(K)(Y)%X!dA!?ߎ,mLU1ƶmr6X@ɺ(#Nڶz> ! zڦ뺪i6#έV+cLYeY6ٽz;*Ƙr 2R<8%1}- vzleY )E~ +Dxa#ZK!yu]3tGtک(x9=t >Los6 y5NJZ;E^Q+~k7SJ J)v=?^ﯲ%X{}^V|os9 )ߧX!:;}8k9WF9(!BuRʦi[3~~8$]Q~;R}}Тa`R"3KB凜B#rΡmۢݍu{wENG؞;|nT4eX ?l(}Q+UUUʹ7ֶm[zJOmq^U֧%`.t5M3L@"uX AE(tB%{±UU1PfYѺszZF]JsRkmLwضs ]}=ĸs;y9+)ReIЋ?s{ߢ(2uU7Am#`ÔV*T2R!䔴19u~ZE(=ۂE)Se_s>#k)g\#,v2 ʤkמhLBsJI*1z1z:{D6u I9D3ːX[NmXE/bo7j0oƮZ[k=}_&sL1d(ˣ6W`Q TUcCk]sdL}}UMP s~ضu"~aQk]nSJBJWJ^봊: ?vu1f*+>oNɸE,+ADq?Ȕhg O4?Gr۶e@9}c}s 7E`w[ԁO)yck2; _8cV78c1c1c} <<<5esJ-ӆlq\)43++fO zsZ]uZ1 ty6ҀD_g"JC̏S_x-@T_4Ĝ^r$"|O_6ۈCmUٴt:j9M19{km _ڶ2#1xX7D)ͦt*| xΜa\eKr{޵*D\,esJAryȱmۆKls:[Um6iͦic*Fl}9F!hF*@s~}>/ӢVTOH S eT3~;/ 'kQ*H70;>5 bϋ܈X@ڃ\J)UZ%lADyN) ÐsSYιWBYUkm%7 ^vj6JcmL^im.$z]H*eq启ʕ7fC$PZoֿUmB)x"J)K* h'h }7-Jw+KF+D\6\?q@9b:?֕lkkWk-IԄ֓TIv$Ms\wiy[!Y:k\{_1ӇcsBf.bF۶JkD}ZfB0u]1F!bov_X]) 7u'k! t`1٬7Rjyj9akr:UQŧ55&N1RʰY*pP7Z.p߂''$ B ŜI/xBLKt:9S& ^RE2V⒉L$j1Dw]۶Mt]״-!)fo8ZkRqۺ.t]w΅Z@<0w+ scL]u]}V H)9rZk[U0}J7x||޿zzzlmF㝋1jc9c;Ft UUrUU!Ho6}߶t:B,pY5mLS)M&^HCX,Jkkm:5BUUe޾7 Bl]W+rvUum9Y -{ XMHxzI_ԙj/(gB#ey u]+I!12"exD֖E7U*V*٦ias]PC]M&i]&`=hz Ķmsex|RRZWUc$2trZٽ{rJJT7(mD!rӚc !*Pu]fl6ιi&RWRl-CJ)beorYRSur!1lIi?י!pcNnDIcʿHĿLp_분ZkkmPMJ䱾eֈy+BUfv9!%xGջdL 5E SQU&95GEE+D#!{ډ"rs_Ayeu]ZV".8ǔ |yp̷A`sRTp^o1]#cː,Ox||wӲX7x_6Mݘ IDAT1ڜPcLDZ"aRڪ*HƸa(7 jl-y !RJι7ژSJ}RJJ c> nF5rcBC3x1p"v Zi3B’hESII!迈ȹȹ5Ca҂&6A|P%4!(2l=:XngU=Ƹj"r !c,.zMEYk `>ژkw9cw1ƮAZd9A?bL'nOr6d@TD$B-˨PME HЂ;Bj 75Lt^vDLk{9 ѫ)) _ژ)9wH?^]u] )W"RJDT?y)S1vxEVA~I4\ $*|?2a2 0;r)la  ;R"m?qL_1J)z>[v2Av*.]S$m5S>6cLB@/%~TJ8(IF vG@ 5 ّ'A{ʞ'!զחjSRM$QH:INS qZi: ֊9 )ߏL1bDOEk)ob˹ґ|_}N;t16$GLJmw1vWEohӜ7Y!?x},5,Pc`zכ(9笤TJINB06MS~rQSHC(ȻFNydcj# i[8lh& N\Q|q‡/m4'@4.>iAIOEV?3v ׿y@ ֺk"" (QkA)FqyMaRe屰$%m*Mb&\6iE߁kՖmC5'tϟ+3 D(|u1A5SOB Az?W]wa6u] 1'"DWo yu6atW;7" ~aKf|4P>pPYQnB/ [b1pH9__]F%" c]s.5F1Ʈ1П1>T^[R0U?1FEW1 0\>0@1+}b=\H~vגoǏO>m%qsE1vGn,϶e(?-=l2 %KAPHQ1˧ ?@?nN=Q>Ƴf2z|| 1. N9yRJ⼣/<">fqoX{+3kmU_^^1v+ G.>Ew9d>AAs@LӇ8T >`z5D|.x/h'0 l}~F<ފzkk-Ojc_B!ф3|}NŠbը4'""H ӛ d2jmmVk/m.K__Kd:-?s-)=F11@J q]M*) K?7MFD"1v]GDZv2~@ӶF맧'")<k+ks{?r¯ :zp^'k6#9???&mV}c9g!7 !Q tJJ[}%c~tGRʜSPm篮kcaXBd9Pq^#4kߎbTJ5MRRJ )I](D 6Y*i4ްlFp1FTUU)X%$xom6R),VUE9Ϡo-sid"TX#v?N8^>]CYk]Z:`W򳵶Lt 2vF|K))~r隈 ]cAX 符yݵױ|kFc(x/RF[沈1z1z:-}Ռ1e!ыŢL2au`Q& k%9Ra[,A6u k=kQa4?ӎ֊9H}_u4VHyny}w qd5j oU@tM|jrV5y??n2RJ$olsaz,^ z!"@V9篛1-68; "~]qRb9Y盀D+r*Yfy&9Y7s/ T%^R#-{&D,}bTJz,cW-d ξI.TW_W042SIC<H¯=Rzكhx׃ΤL^c1;‹p?=c1ܵL>c1П1J7W2cP Cc+ad jhA|W!gP5;<1oǏ cde yʳǶh3V#u&t:}xxyػCrAC,[B?,<_p}7v OI)} ^ju]=}S>HE]Je ^ G aWDN&0J-պqoJZԕ1A+'7e3a<ѷ_PMS)%jl6 !!Rs~~~d:-ݵmFDهw;RN=Zi[0e꺶U5y2Q~:"OS@5volF#"B4mJ.cy&Iy}Qs^kFh(U!Wn)%UN&RƏu}%3v NI[Ks~zz^k]|]Rϟn["imL[,bJaÔү? D޹<.ƘR*yZ޸*mwn1} I)wUԮM'Szo)w9u4eS4JaB ^*)F4J)}u(AFv& k0 Jk[ΪQ׵ևX,HC !ҺZpڵCƾcq ?*Yj1Fkʣ@&ZeFPK潟f;ZiCPJ )~B rΕ rlhWEtCRJ R!ұ$J6ik NJuJlJ)M+OFQ90 Z%[>-Y"xLky cC>.\`{Ѵzcp܇MSWDN8tcYm~`9g!6f:v#aUe!Xk萸{yxxH)圝sށyWԮM )%"*߅RJ@\-t#ŀSN)ɟ|ԳzBBOrοϔG.:a}ppof).X !( )@z;1J!fRJiȅkQQϜ} /^^bh$́u()gVrEAg_hbph@ 8<ӹ]]Zbv۶9'mQ:Qιt)CE%$UV^./UQ6SoO!)% ZuWJm.)e:H1`K:}!~qEcX&5IB|W+"+$)=%#ޥڶu)FFW¯i[?CڦibŸ"笤, 2 [B)icrJAF!bD1 @JIi] e(ĝc9:rX۶mRŏ1jJ9FN&dA')Fׂ5RQH~^!تj&`T])q,ط2;%)+"&Rw) *h#ewNS ʼ.c-;|\ι& @_zBRig7 ~&0H1e7Y}kEtNm>eUu1@²ŢLT0!]}֌)FuBR1?nOi[q}ifN7]3jiG|ws,P `'{|v>YĊA܇FhCl0>VbvC.RJ)MvmV37)]Pabh w. (o6oVHȳBi0K̋ǤRzٹhx׹v]B"[UDeP ?N2#G>Gx>y,.1?rP.'wѭ8"ʝ_c1}3c1v'dVcрI?_RMG~6`vKR]no=Y?[}\Svfc⢅F#ն6ϟZttN&Z7}+/B}LB2V'w7G" =O#r"sb_e$?MJphRJbߢ+~c7nݼX|DBDwsF@r!9) RJ)1FӇcsZ#"#"D>,oWTȱN+9/g9czڴw|^#F*J+b,[rΫbhKJpPR aC34Fk6N;)ulyq$EI<וBU7(78 sLRC٘nDϣy@!N`D7lMGve+yYߍ6CYU)%"*w}K)KR$%"¿9I}NI{1)svxEb\9{]kv|^TW{1C)rr8ٮe"ξik ǂ㿾~p scLy*G(7m7VR/@DfU=EurGS ̨ 0SGynfTGX\\zD,/sxTyNxEb\#1=m/ :/Gn/|P" C0kFA `"A <_[c3-:WJ*,z繈Gu31bWҶ-t]u?Ó\9/!Dui*_>sΥoI~{~oEk1) !VU':ܺi;EH,!R[_Jyfb)D{NxnՇU;mfqS^"oo[komv>C&'HEa&8ibI ߽VK(n>A!Vi2N sVR*JsmۆQ*%Kٺ}yژ_m !kwb۶9TjUҺ,R5 C PN-xE:bl5S~[QR]uiO͏:ERUUmc['"#`]kHF*(Qf4LlU5MBĸ# `Fjc'7cy= ~}DbIC IDATv Po _x$D7(mr>9WUd2Utu1@WJUb\]&4]!J1"by֦A*e1ƔK-D2Z0ylwUcw2,;=7MS*j5wdx͏;Ebh'L.)aʑbt]'(uc\OeNUUC(H9qNc;i'@gb JBn^P ϯ.áwIkӶ- 1y#>fymfmYEDD(<@2U(s[>ɬM U-Dbe(#Dn& ʩ7ԩ5S_N@[2~D{UU0ͺ :lZO钄Bx.9kvKvni׎'N|G(@uɯ44 atm> AYi0rRzYe8 b>_{]dO]oϾN2#G>Gx[S"/DHsv源#a |1[ 0]П1&1n1#XKh$]Ȯ6&aLE~m>qZeUkr^)3caF^Go>zcJIo.ȚRkcN)}ODJUK)}OOOڪmUa)a١1{BRVUU7MXQum6Mu]ӶX,O1c*ISATW7 A _/^*0FN1{?͌1;ZiC(pjZDT y`ቨlZܻֈ6&.wX^PsZDTRv]s> c18dIwWw3& ( L "`g FÀዖHU9צ19үoalUYcbZ~"D/<k[_Om[s߯ZLnV/V%8^*1cs(7 oRU]۪rMc!)eUUutkaVUUic0J' sc P*^TjS4\Qc)9V4ƬnfR*7?+ !@1ƔzUZuJiÓ+1>gTMI a&CnKzQa*)K쬍)Yj(VBs. 8"*|]7cViYBl63Ƭ1bă9t]f?U]+cJ)!尺 ]'Wc}/c_JP|'"f@IRvyc97D)%"**^R9;CrΫ9"MBlaS:EJ鐮>+0تDT:<cC1;j U xFct?axo_1J)z>_2#fV(PJKo:b#"oZ=c 1~/;q? @xT7[UqB0s.S 1Z벓!nBg7 !VH/*ae4e(3xom6R)>Vu]1"2@ZI))1nYN m[R nR!cm١6^sVR*rD>sQ|BNIFGpc1;1z1z:u1@H+e.?aXuwA*e1ƔPx_~4Яg7WCDmr>X9WUd27{;̷!X)7XFpc poa ,_^^~#aD4< S}&㍋ݓ۸ -p9cϛC^a175XoyN/)M%l&'(-/ d%wڶ)P 6a#9n/;RJi17؀1ƮG'z&&.)ӝ;oNؔ8(m )sJ[c1ƾD5/Aj2[J])S쯹܀wt%1صru^3m:\۶ژ_?d/)"2E1nѡK4 )%Mu]{/xmU_^^VB4M#B!,Ǐ7{XŲZkyJb@4Ds0#&"}w\ c10 CUum7oF)5 CJIkmmV/CĪ.X[uyprRY!5's&"*"*vr1c1R (u  ^O+&өU$//%]c6J)a1x߭0Ns)Iz)l5wn}?d-Fo]? 0ጷ# h$`"1 I KDxPc hb&@ \rV՞U՗~B޿5~k-)KKKP?~a{1eYY1!%glNPqC%w.ߥXߔeTꬭ-BGta 9%6t>ՃíEY~ᜓBH)CfZX2(Z%e$Z)" U#(8'*$Ju"QkzaQQqdFY^ͦ6tcUwk#SN RJ&M\C[O57q/cV~ب8I(Z',˲ *ISkRI ¿D J!FTW$IRKK'_ /24d0~ey[wl61;疗YyoCXGQIu}vl6as5nRzocU7B oacF糞yo<~^SQkѯ2TA< F֓ C[?b=S#dK!ɂcG{1:Y[rOd-v[WT5{:BwtTOazح!:R'I=dYOޓ`ɯRLgi{.'%6|&^e-rp܌TbZ/RfcLuɋyYtfY&bֹZv W?74쐑l(WR1e_ڳ,SJy>RJQTRj%sn7EunՏZmy9lRJcm\1έEQ8R:y{;Nqw4Mhj2Ƭy4YkJjK)Z$Ih"'I…yoJΕe{v(B 0eYJT眼9GDUDE'2m'!爞#"Egm5A܁,Zփe$`&蜫jcVQ8IV8?~|K#?V8I,wv̯3#WV]c0-9o4yy(zt9qs!x!c׸ޓf6luVo.,c:v$R(<_XXBe(xei:Vkc5ͪ$i@e'fDcɑ,U͌TzST}<㉺bk.<)F%*Ã~-#}9ZU3Xo,BUQ|(Q'QɀP3pB7Za^v 6f,L*sJ)y,yүReDI nsNiI;G8wMc"ۍ7`y}f L?L9:LAM3 g[T ?L R~6ZVU-2ɤ`2e9Ặa}бf&?dY>vN[RHŒʮ Kra\Yg);v,}Ѳ,{҉cØyU$R F~)#^ZlLwHJh6あ4MEQE2D U_74V%%&̅S?/̩ai [D=MR3 !ʢX^^Z'I$IA4Bt]\nj9yu;km[VQi.,K!DI!Gk8ykK[dt3톾xi19$"c̱G$x`NS5GQű߰=QcOKĘ*##|6$W|4Enkg5{kd4ܙlKz_ʈ;m Z(<6{s|_dK6clpw';"sWA9[USY$ X}'C !cRV( ‚9$Mų /etC/Mw6۪U$'j= \IEQf;`o!"b9㊎QeL_h0<ϭ,&1#(f8y1f RJZ^^7ƈ/tgcz!s9Шl%yEJIk Ocic aAJllK/eK0a}#FsR~0d}Oz-ԢV6:#LZ+BTƲ,\J)5%eֹd@ORZK2WBJk cY:,+˒1&䌅Fk$I,Zk w˜yF1axi٘$>Ip8IRN;>r8WͦZ&zB:MK'=㬾h*fn;v#J)9,(BaREBR{Jkc޹,{^Jq)LT3*˒qQdcB*9X#H)cOcė2I99'$tkQ\REQ$8D (D"dkfX;cD^uAcT-v[EkFө5M؉&1K0/`^YU)79؅fP:Yn>Alǎ`&foR}8>-0 ZTJ1ݸ+]RkEsES RX 0QJ)1c89y$B|;;8Q )~|{S[8@ZJY[06qE!Z0lRrX"c`Q4nl C?! ``7;V0lR=?v~SR=?̟W(7;3ć?᫮smۿ~?~|QUs?o?_|'?"|?[}P0}>WJ]~\p?q}~'v6{,<>s|m8(R`"sUW]u:t:NHs7xn!O{~ /7ᄃz.|3DtW_}{O~1|0p,!C=8pn{g?я~_=G/kKڷ-7Z[ni裏~#?}%{W_}׾5pt0coۏ9Ç_wugq?*m?}{<̇>n+_g}뭷tM޷oѣG;믿_by7G?:;#s=z7xȑ/Y_s5Qs=__z*}s_җv{~_7tM/?}cyG.҇~?0C[_}՟gDtȑGqƘv3ѳ>{YgѻD/%\9[HLxwqGUO|#G' oxCv<]yO뮻o|]w|giii򗿼?w#VmV袋$_|qE׿袋BwD_| ^jcy{cD+~iÓuƘFɮIH`B=W\q뭷yarرcfC=s1رcÇo}[J(~GnƋ/xu]v}w݁`z衻ᄏwo~U0՛~k O꫏>>/>ځ~_|W^y-ܲ])@\9F*wZcq1󝎂(Z^tvyW_}tϲy(qj?Zh_( y0{SNG ZYι0ܹH!) ,3(T:>`D0 H`BhoH`" #include "Logger.hpp" #define ASSERT(expr) \ if (!(expr)) { \ g_logger->log(LOG_CRIT, "Failed assertion at line {} in {}: {} was false", __LINE__, \ ([]() constexpr -> std::string { return std::string(__FILE__).substr(std::string(__FILE__).find("/src/") + 1); })(), #expr); \ std::abort(); \ } hyprshutdown-0.1.1/src/helpers/Logger.hpp000066400000000000000000000005671520067112300204550ustar00rootroot00000000000000#pragma once #include #include "Memory.hpp" #define LOG_DEBUG Hyprutils::CLI::LOG_DEBUG #define LOG_ERR Hyprutils::CLI::LOG_ERR #define LOG_WARN Hyprutils::CLI::LOG_WARN #define LOG_TRACE Hyprutils::CLI::LOG_TRACE #define LOG_CRIT Hyprutils::CLI::LOG_CRIT inline UP g_logger = makeUnique(); hyprshutdown-0.1.1/src/helpers/Memory.hpp000066400000000000000000000005231520067112300204760ustar00rootroot00000000000000#pragma once #include #include #include #include #include using namespace Hyprutils::Memory; #define SP CSharedPointer #define WP CWeakPointer #define UP CUniquePointer #define ASP CAtomicSharedPointerhyprshutdown-0.1.1/src/helpers/OS.cpp000066400000000000000000000071361520067112300175510ustar00rootroot00000000000000#include "OS.hpp" #include #include #include #include #if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) #include #if defined(__DragonFly__) #include // struct kinfo_proc #elif defined(__FreeBSD__) #include // struct kinfo_proc #endif #if defined(__NetBSD__) #undef KERN_PROC #define KERN_PROC KERN_PROC2 #define KINFO_PROC struct kinfo_proc2 #else #define KINFO_PROC struct kinfo_proc #endif #if defined(__DragonFly__) #define KP_PPID(kp) kp.kp_ppid #elif defined(__FreeBSD__) #define KP_PPID(kp) kp.ki_ppid #else #define KP_PPID(kp) kp.p_ppid #endif #endif static std::optional linuxExtractFromStatus(std::ifstream& ifs, const std::string_view& what) { std::string line; std::string full = std::string{what} + ":"; while (std::getline(ifs, line)) { if (!line.starts_with(full)) continue; return Hyprutils::String::trim(line.substr(full.size())); } return std::nullopt; } std::string OS::appNameForPid(int64_t pid) { #if defined(KERN_PROC_PID) int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, Hyprutils::Memory::sc(pid)}; KINFO_PROC kp; size_t len = sizeof(kp); if (sysctl(mib, 4, &kp, &len, nullptr, 0) == -1) return ""; if (len == 0) return ""; #if defined(__FreeBSD__) || defined(__DragonFly__) return kp.ki_comm; #elif defined(__NetBSD__) || defined(__OpenBSD__) return kp.p_comm; #endif #else std::string dir = "/proc/" + std::to_string(pid) + "/status"; std::ifstream ifs(dir); if (!ifs.good()) return ""; auto data = linuxExtractFromStatus(ifs, "Name"); return data.value_or(""); #endif } std::vector OS::getAllPids() { std::vector pids; #if defined(KERN_PROC_PID) int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0}; size_t len = 0; std::vector procs; if (sysctl(mib, 4, nullptr, &len, nullptr, 0) == -1) return {}; procs.resize(len / sizeof(kinfo_proc)); if (sysctl(mib, 4, procs.data(), &len, nullptr, 0) == -1) return {}; pids.reserve(procs.size()); for (const auto& p : procs) { pids.emplace_back(p.ki_pid); } #else std::error_code ec; if (std::filesystem::exists("/proc/self", ec) && !ec) { for (const std::filesystem::path& p : std::filesystem::directory_iterator("/proc/", ec)) { if (!std::filesystem::exists(p, ec) || ec) continue; if (!Hyprutils::String::isNumber(p.stem().string())) continue; try { pids.emplace_back(std::stoll(p.stem())); } catch (...) { ; } } } #endif return pids; } int64_t OS::ppidOf(int64_t pid) { #if defined(KERN_PROC_PID) int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, (int)pid, #if defined(__NetBSD__) || defined(__OpenBSD__) sizeof(KINFO_PROC), 1, #endif }; u_int miblen = sizeof(mib) / sizeof(mib[0]); KINFO_PROC kp; size_t sz = sizeof(KINFO_PROC); if (sysctl(mib, miblen, &kp, &sz, nullptr, 0) != -1) return KP_PPID(kp); #else std::string dir = "/proc/" + std::to_string(pid) + "/status"; std::ifstream ifs(dir); if (!ifs.good()) return -1; auto data = linuxExtractFromStatus(ifs, "PPid"); if (!data) return -1; try { return std::stoll(*data); } catch (std::exception& e) { ; } #endif return -1; } hyprshutdown-0.1.1/src/helpers/OS.hpp000066400000000000000000000003411520067112300175450ustar00rootroot00000000000000#pragma once #include #include #include namespace OS { std::vector getAllPids(); std::string appNameForPid(int64_t pid); int64_t ppidOf(int64_t pid); };hyprshutdown-0.1.1/src/main.cpp000066400000000000000000000073461520067112300165150ustar00rootroot00000000000000#include "helpers/Asserts.hpp" #include "ui/UI.hpp" #include "state/AppState.hpp" #include #include #include #include #include #include #include using namespace Hyprutils::OS; // fork off of the parent process, so we don't get killed static void forkoff() { pid_t pid = fork(); if (pid < 0) exit(EXIT_FAILURE); if (pid > 0) exit(EXIT_SUCCESS); if (setsid() < 0) exit(EXIT_FAILURE); signal(SIGHUP, SIG_IGN); pid = fork(); if (pid < 0) exit(EXIT_FAILURE); if (pid > 0) exit(EXIT_SUCCESS); umask(0); } int main(int argc, const char** argv, const char** envp) { Hyprutils::CLI::CArgumentParser parser({argv, sc(argc)}); ASSERT(parser.registerBoolOption("dry-run", "", "Do not exit apps, only show UI")); ASSERT(parser.registerBoolOption("no-exit", "", "Do not exit hyprland once apps close")); ASSERT(parser.registerStringOption("top-label", "t", "Set the text appearing on top (set to \"Shutting down...\" by default)")); ASSERT(parser.registerStringOption("post-cmd", "p", "Set a command ran after all apps and Hyprland shut down")); ASSERT(parser.registerBoolOption("verbose", "", "Enable more logging")); ASSERT(parser.registerBoolOption("no-fork", "", "Do not fork/daemonize (run in foreground)")); ASSERT(parser.registerIntOption("vt", "", "Switch to VT N after Hyprland exits (fixes NVIDIA+SDDM black screen)")); ASSERT(parser.registerBoolOption("help", "h", "Show the help menu")); if (const auto ret = parser.parse(); !ret) { g_logger->log(LOG_ERR, "Failed parsing arguments: {}", ret.error()); return 1; } if (parser.getBool("help").value_or(false)) { std::println("{}", parser.getDescription(std::format("hyprshutdown v{}", HYPRSHUTDOWN_VERSION))); return 0; } if (parser.getBool("verbose").value_or(false)) g_logger->setLogLevel(LOG_TRACE); if (parser.getBool("dry-run").value_or(false)) State::state()->m_dryRun = true; const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE"); if (!HIS || HIS[0] == '\0') { g_logger->log(LOG_ERR, "Cannot run under a non-hyprland environment"); return 1; } // By default, hyprshutdown forks to avoid being killed when the parent terminal closes. // The --no-fork option runs in the foreground, useful for debugging or scripting. if (!parser.getBool("no-fork").value_or(false)) forkoff(); else { g_logger->log(LOG_DEBUG, "Skipping fork due to --no-fork option"); signal(SIGHUP, SIG_IGN); // Still ignore SIGHUP to survive terminal disconnect } if (!State::state()->init()) { g_logger->log(LOG_ERR, "Failed to init state"); return 1; } g_ui = makeUnique(); g_ui->m_noExit = parser.getBool("no-exit").value_or(false) || State::state()->m_dryRun; g_ui->m_shutdownLabel = parser.getString("top-label").value_or("Shutting down..."); g_ui->m_postExitCmd = parser.getString("post-cmd"); // Capture VT switch option before running UI auto vtSwitch = parser.getInt("vt"); g_ui->run(); // VT switch for NVIDIA+SDDM: after Hyprland exits, the display may not // automatically switch back to the greeter's VT, causing a black screen. // This explicitly switches to the specified VT to fix it. if (vtSwitch && *vtSwitch > 0 && !State::state()->m_dryRun) { g_logger->log(LOG_DEBUG, "Switching to VT{}", *vtSwitch); std::string cmd = std::format("sudo -n chvt {}", *vtSwitch); CProcess proc("/bin/sh", {"-c", cmd}); proc.runAsync(); } return 0; } hyprshutdown-0.1.1/src/state/000077500000000000000000000000001520067112300161735ustar00rootroot00000000000000hyprshutdown-0.1.1/src/state/AppState.cpp000066400000000000000000000217571520067112300204340ustar00rootroot00000000000000#include "AppState.hpp" #include "HyprlandIPC.hpp" #include "../helpers/Logger.hpp" #include "../helpers/OS.hpp" #include #include #include #include using namespace State; static const std::vector IGNORE_DAEMONS = { "Xwayland", }; SP State::state() { static auto state = makeShared(); return state; } CApp::CApp(glz::generic::object_t& object) { if (object.contains("address")) m_address = object["address"].get_string(); if (object.contains("title")) m_title = object["title"].get_string(); if (object.contains("class")) m_class = object["class"].get_string(); if (object.contains("namespace")) { m_class = object["namespace"].get_string(); m_alwaysUsePid = true; // layers cant be closewindow'd } if (object.contains("xwayland")) m_xwayland = object["xwayland"].get_boolean(); if (object.contains("pid")) m_pid = sc(object["pid"].get_number()); } CApp::CApp(const std::string& name, int pid) : m_class(name), m_pid(pid), m_alwaysUsePid(true) { ; } void CApp::quit() { if (!m_alwaysUsePid && (!m_address.empty() || m_pid <= 0)) { // for apps that have an address, use closewindow. Some apps don't ask for saving on SIGTERM if (m_address.empty()) { g_logger->log(LOG_WARN, "CApp::quit: app {} has no address and no valid pid, skipping", m_class); return; } g_logger->log(LOG_TRACE, "CApp::quit: using close for {}", m_class); std::string cmd; if (State::state()->m_useLua) cmd = std::format("/dispatch hl.dsp.window.close({{ window = 'address:{}' }})", m_address); else cmd = std::format("/dispatch closewindow address:{}", m_address); auto ret = HyprlandIPC::getFromSocket(cmd); if (!ret) g_logger->log(LOG_ERR, "Failed closing window {}: ipc err", m_class); else if (*ret != "ok") g_logger->log(LOG_ERR, "Failed closing window {}: {}", m_class, *ret); } else { // SIGTERM with pid if (m_pid <= 0) { g_logger->log(LOG_WARN, "CApp::quit: app {} has invalid pid {}, skipping SIGTERM", m_class, m_pid); return; } g_logger->log(LOG_TRACE, "CApp::quit: using SIGTERM for {}, pid {}", m_class, m_pid); if (::kill(m_pid, SIGTERM) != 0) g_logger->log(LOG_ERR, "CApp::quit: signal failed for pid {}, err: {}", m_pid, strerror(errno)); } } void CApp::kill() { if (m_pid <= 0) { g_logger->log(LOG_TRACE, "Can't kill {}: no pid", m_class); return; } g_logger->log(LOG_TRACE, "CApp::kill: killing {}, pid {}", m_class, m_pid); if (::kill(m_pid, SIGKILL) != 0) g_logger->log(LOG_ERR, "CApp::quit: signal failed for pid {}, err: {}", m_pid, strerror(errno)); } bool CApp::appAlive() const { if (m_pid <= 0) return false; if (::kill(m_pid, 0) == 0) return true; if (errno == EPERM) return true; return false; } bool CApp::operator==(const glz::generic& object) const { if (!object.contains("address")) return false; return m_address == object["address"].get_string(); } bool CAppState::init() { // detect config provider { const auto RET = HyprlandIPC::getFromSocket("j/status"); if (RET) { auto jsonRaw = glz::read_json(*RET); if (jsonRaw && jsonRaw->get_object().contains("configProvider")) { std::string provider = jsonRaw->get_object()["configProvider"].get_string(); m_useLua = (provider == "lua"); g_logger->log(LOG_DEBUG, "Detected config provider: {}", m_useLua ? "lua" : "hyprlang"); } } } // windows { const auto RET = HyprlandIPC::getFromSocket("j/clients"); if (!RET) { g_logger->log(LOG_ERR, "Couldn't get clients from socket"); return false; } auto jsonRaw = glz::read_json(*RET); if (!jsonRaw) { g_logger->log(LOG_ERR, "Socket returned bad data"); return false; } auto jsonArr = jsonRaw->get_array(); m_apps.reserve(jsonArr.size()); for (auto& el : jsonArr) { m_apps.emplace_back(makeUnique(el.get_object())); } } // layers { const auto RET = HyprlandIPC::getFromSocket("j/layers"); if (!RET) { g_logger->log(LOG_ERR, "Couldn't get layers from socket"); return false; } auto jsonRaw = glz::read_json(*RET); if (!jsonRaw) { g_logger->log(LOG_ERR, "Socket returned bad data"); return false; } for (auto& [m, obj] : jsonRaw->get_object()) { for (auto& [m2, obj2] : obj["levels"].get_object()) { for (auto& el : obj2.get_array()) { m_apps.emplace_back(makeUnique(el.get_object())); } } } g_logger->log(LOG_DEBUG, "Parsed {} apps from socket", m_apps.size()); } // children of the Hyprland process // TODO: make a kernel cgroup in hl. This can miss things. // Maybe keep this for BSDs, which don't do cgroups, once we figure out PPid on BSDs. { const auto INSTANCES = HyprlandIPC::instances(); const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE"); if (HIS && HIS[0] != '\0') { const HyprlandIPC::SInstanceData* instance = nullptr; for (const auto& I : INSTANCES) { if (I.id != HIS) continue; instance = &I; break; } if (!instance) g_logger->log(LOG_ERR, "Can't get children: no instance??"); else { // get all processes that have a PPid of us const auto PROCS = OS::getAllPids(); for (const auto& pid : PROCS) { // check if child if (OS::ppidOf(pid) != instance->pid) continue; const auto NAME = OS::appNameForPid(pid); if (std::ranges::contains(IGNORE_DAEMONS, NAME)) continue; m_apps.emplace_back(makeUnique(NAME, pid)); } } } else g_logger->log(LOG_ERR, "Can't get children: no HIS"); } // exit them if not dry run if (!m_dryRun) { for (const auto& e : m_apps) { e->quit(); } } return true; } const std::vector>& CAppState::apps() const { return m_apps; } float CAppState::secondsPassed() const { return std::chrono::duration_cast(std::chrono::steady_clock::now() - m_started).count() / 1000.F; } bool CAppState::updateState() { const auto RET = HyprlandIPC::getFromSocket("j/clients"); if (!RET) { g_logger->log(LOG_ERR, "Couldn't get clients from socket"); return false; } auto jsonRaw = glz::read_json(*RET); if (!jsonRaw) { g_logger->log(LOG_ERR, "Socket returned bad data"); return false; } auto table = jsonRaw->get_array(); const auto BEFORE = m_apps.size(); std::erase_if(m_apps, [&table](const auto& e) { return !e->appAlive() && !std::ranges::any_of(table, [&e](const auto& te) { return te == *e; }); }); // check PIDs if (!m_dryRun) { for (const auto& app : m_apps) { if (!app->appAlive() || app->m_pid <= 0 || app->m_address.empty() /* not a window */ || std::ranges::contains(m_pidsTermedNoWindows, app->m_pid)) continue; const bool HAS_ANY_WINDOWS = std::ranges::any_of(table, [&app](const auto& te) { if (!te.contains("pid")) return false; return sc(te["pid"].get_number()) == app->m_pid; }); if (HAS_ANY_WINDOWS) continue; // app has no windows, but is alive. Send a SIGTERM. // TODO: maybe make this also repeat every 5s or so? m_pidsTermedNoWindows.emplace_back(app->m_pid); g_logger->log(LOG_DEBUG, "App {} with pid {} window was closed, but pid is alive. Sending SIGTERM.", app->m_class, app->m_pid); kill(app->m_pid, SIGTERM); } } g_logger->log(LOG_DEBUG, "Updated state: apps size {}", m_apps.size()); return BEFORE != m_apps.size(); } void CAppState::killAllApps() const { if (m_dryRun) { g_logger->log(LOG_TRACE, "CAppState::killAllApps: ignoring, dry run"); return; } for (const auto& a : m_apps) { a->kill(); } } void CAppState::reexitApps() const { if (m_dryRun) { g_logger->log(LOG_TRACE, "CAppState::reexitApps: ignoring, dry run"); return; } for (const auto& a : m_apps) { a->quit(); } } hyprshutdown-0.1.1/src/state/AppState.hpp000066400000000000000000000032601520067112300204260ustar00rootroot00000000000000#pragma once #include "../helpers/Memory.hpp" #include #include #include namespace State { class CApp { public: CApp(glz::generic::object_t& object); CApp(const std::string& name, int pid); ~CApp() = default; CApp(const CApp&) = delete; CApp(CApp&) = delete; CApp(CApp&&) = delete; bool appAlive() const; bool operator==(const glz::generic& object) const; void quit(); void kill(); std::string m_address; std::string m_title; std::string m_class; int64_t m_pid = -1; bool m_xwayland = false; bool m_alwaysUsePid = false; }; class CAppState { public: CAppState() = default; ~CAppState() = default; bool m_useLua = false; CAppState(const CAppState&) = delete; CAppState(CAppState&) = delete; CAppState(CAppState&&) = delete; bool init(); bool updateState(); float secondsPassed() const; void killAllApps() const; void reexitApps() const; const std::vector>& apps() const; bool m_dryRun = false; private: std::vector> m_apps; std::vector m_pidsTermedNoWindows; std::chrono::steady_clock::time_point m_started = std::chrono::steady_clock::now(); }; SP state(); }; hyprshutdown-0.1.1/src/state/HyprlandIPC.cpp000066400000000000000000000116011520067112300210130ustar00rootroot00000000000000#include "HyprlandIPC.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Hyprutils::Memory; static int getUID() { const auto UID = getuid(); const auto PWUID = getpwuid(UID); return PWUID ? PWUID->pw_uid : UID; } static std::string getRuntimeDir() { const auto XDG = getenv("XDG_RUNTIME_DIR"); if (!XDG) { const std::string USERID = std::to_string(getUID()); return "/run/user/" + USERID + "/hypr"; } return std::string{XDG} + "/hypr"; } static std::optional toUInt64(const std::string_view str) { uint64_t value = 0; const auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), value); if (ec != std::errc() || ptr != str.data() + str.size()) return std::nullopt; return value; } std::expected HyprlandIPC::getFromSocket(const std::string& cmd) { static const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE"); if (!HIS || HIS[0] == '\0') return std::unexpected("HYPRLAND_INSTANCE_SIGNATURE empty: are we under hyprland?"); const auto SERVERSOCKET = socket(AF_UNIX, SOCK_STREAM, 0); auto t = timeval{.tv_sec = 5, .tv_usec = 0}; setsockopt(SERVERSOCKET, SOL_SOCKET, SO_RCVTIMEO, &t, sizeof(struct timeval)); if (SERVERSOCKET < 0) return std::unexpected("couldn't open a socket (1)"); auto socketGuard = Hyprutils::Utils::CScopeGuard([&] { close(SERVERSOCKET); }); sockaddr_un serverAddress = {0}; serverAddress.sun_family = AF_UNIX; std::string socketPath = getRuntimeDir() + "/" + HIS + "/.socket.sock"; strncpy(serverAddress.sun_path, socketPath.c_str(), sizeof(serverAddress.sun_path) - 1); if (connect(SERVERSOCKET, rc(&serverAddress), SUN_LEN(&serverAddress)) < 0) return std::unexpected(std::format("couldn't connect to the hyprland socket at {}", socketPath)); auto sizeWritten = write(SERVERSOCKET, cmd.c_str(), cmd.length()); if (sizeWritten < 0) return std::unexpected("couldn't write (4)"); std::string reply = ""; char buffer[8192] = {0}; sizeWritten = read(SERVERSOCKET, buffer, 8192); if (sizeWritten < 0) { if (errno == EWOULDBLOCK) return std::unexpected("Hyprland IPC didn't respond in time"); return std::unexpected("couldn't read (5)"); } reply += std::string(buffer, sizeWritten); while (sizeWritten == 8192) { sizeWritten = read(SERVERSOCKET, buffer, 8192); if (sizeWritten < 0) { return std::unexpected("couldn't read (5)"); } reply += std::string(buffer, sizeWritten); } return reply; } static std::optional parseInstance(const std::filesystem::directory_entry& entry) { if (!entry.is_directory()) return std::nullopt; const auto lockPath = entry.path() / "hyprland.lock"; std::ifstream ifs(lockPath); if (!ifs.is_open()) return std::nullopt; HyprlandIPC::SInstanceData data; data.id = entry.path().filename().string(); const auto first = std::string_view{data.id}.find_first_of('_'); const auto last = std::string_view{data.id}.find_last_of('_'); if (first == std::string_view::npos || last == std::string_view::npos || last <= first) return std::nullopt; auto time = toUInt64(std::string_view{data.id}.substr(first + 1, last - first - 1)); if (!time) return std::nullopt; data.time = *time; std::string line; if (!std::getline(ifs, line)) return std::nullopt; auto pid = toUInt64(std::string_view{line}); if (!pid) return std::nullopt; data.pid = *pid; if (!std::getline(ifs, data.wlSocket)) return std::nullopt; if (std::getline(ifs, line) && !line.empty()) return std::nullopt; // more lines than expected return data; } std::vector HyprlandIPC::instances() { std::vector result; std::error_code ec; const auto runtimeDir = getRuntimeDir(); if (!std::filesystem::exists(runtimeDir, ec) || ec) return result; std::filesystem::directory_iterator it(runtimeDir, std::filesystem::directory_options::skip_permission_denied, ec); if (ec) return result; for (const auto& el : it) { if (auto instance = parseInstance(el)) result.emplace_back(std::move(*instance)); } std::erase_if(result, [](const auto& el) { return kill(el.pid, 0) != 0 && errno == ESRCH; }); std::ranges::sort(result, {}, &SInstanceData::time); return result; } hyprshutdown-0.1.1/src/state/HyprlandIPC.hpp000066400000000000000000000006151520067112300210230ustar00rootroot00000000000000#pragma once #include #include #include #include namespace HyprlandIPC { struct SInstanceData { std::string id; uint64_t time; int64_t pid; std::string wlSocket; }; std::expected getFromSocket(const std::string& cmd); std::vector instances(); }; hyprshutdown-0.1.1/src/ui/000077500000000000000000000000001520067112300154705ustar00rootroot00000000000000hyprshutdown-0.1.1/src/ui/UI.cpp000066400000000000000000000274621520067112300165240ustar00rootroot00000000000000#include "UI.hpp" #include "../helpers/Logger.hpp" #include "../state/AppState.hpp" #include "../state/HyprlandIPC.hpp" #include #include #include #include #include using namespace Hyprutils::OS; namespace { using ButtonPtr = Hyprutils::Memory::CSharedPointer; constexpr float kButtonBaseHeight = 25.F; constexpr float kButtonFontScale = 0.40F; constexpr float kButtonCharWidthFactor = 0.6F; float buttonWidthForLabel(std::string_view label, float padding, float fontSize) { const float textWidth = static_cast(label.size()) * (fontSize * kButtonCharWidthFactor); return textWidth + (std::max(0.F, padding) * 2.F); } template > ButtonPtr makeButton(std::string_view label, OnClick&& onClick, float padding = 0.F, Configure configure = {}) { padding = std::max(0.F, padding); const float buttonHeight = kButtonBaseHeight + (padding * 2.F); const float fontSize = std::max(10.F, buttonHeight * kButtonFontScale); auto btn = Hyprtoolkit::CButtonBuilder::begin() ->label(std::string{label}) ->fontSize(Hyprtoolkit::CFontSize{Hyprtoolkit::CFontSize::HT_FONT_ABSOLUTE, fontSize}) ->size({Hyprtoolkit::CDynamicSize::HT_SIZE_ABSOLUTE, Hyprtoolkit::CDynamicSize::HT_SIZE_ABSOLUTE, {buttonWidthForLabel(label, padding, fontSize), buttonHeight}}) ->onMainClick(std::forward(onClick)) ->commence(); if (configure) { configure(btn); } return btn; } } CUI::CUI() = default; CUI::~CUI() = default; CMonitorState::SAppListApp::SAppListApp(const std::string_view& clazz, const std::string_view& title) { m_null = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_AUTO, {1.F, 1.F}})->commence(); m_null->setMargin(4); m_layout = Hyprtoolkit::CColumnLayoutBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_AUTO, {1.F, 1.F}})->gap(2)->commence(); m_title = Hyprtoolkit::CTextBuilder::begin() ->text(std::format("{}", title)) ->color([] { return g_ui->backend()->getPalette()->m_colors.text; }) ->fontSize(Hyprtoolkit::CFontSize{Hyprtoolkit::CFontSize::HT_FONT_TEXT}) ->commence(); m_class = Hyprtoolkit::CTextBuilder::begin() ->text(std::string{clazz}) ->color([] { return g_ui->backend()->getPalette()->m_colors.text; }) ->fontSize(Hyprtoolkit::CFontSize{Hyprtoolkit::CFontSize::HT_FONT_H3}) ->commence(); m_titleNull = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_AUTO, {1, 1}})->commence(); m_classNull = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_AUTO, {1, 1}})->commence(); m_class->setPositionMode(Hyprtoolkit::IElement::HT_POSITION_ABSOLUTE); m_class->setPositionFlag(Hyprtoolkit::IElement::HT_POSITION_FLAG_LEFT, true); m_class->setPositionFlag(Hyprtoolkit::IElement::HT_POSITION_FLAG_VCENTER, true); m_title->setPositionMode(Hyprtoolkit::IElement::HT_POSITION_ABSOLUTE); m_title->setPositionFlag(Hyprtoolkit::IElement::HT_POSITION_FLAG_LEFT, true); m_title->setPositionFlag(Hyprtoolkit::IElement::HT_POSITION_FLAG_VCENTER, true); m_titleNull->addChild(m_title); m_classNull->addChild(m_class); m_layout->addChild(m_classNull); m_layout->addChild(m_titleNull); m_null->addChild(m_layout); } CMonitorState::CMonitorState(SP output) : m_monitorName(output->port()) { m_window = Hyprtoolkit::CWindowBuilder::begin() ->type(Hyprtoolkit::HT_WINDOW_LAYER) ->prefferedOutput(output) ->anchor(0xF) ->layer(3) ->preferredSize({0, 0}) ->exclusiveZone(-1) ->appClass("hyprshutdown") ->commence(); m_bg = Hyprtoolkit::CRectangleBuilder::begin() ->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}}) ->color([] { auto col = g_ui->backend()->getPalette()->m_colors.background; col.a *= 0.9F; return col; }) ->commence(); m_null = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {0.5F, 0.8F}})->commence(); m_null->setPositionMode(Hyprtoolkit::IElement::HT_POSITION_ABSOLUTE); m_null->setPositionFlag(Hyprtoolkit::IElement::HT_POSITION_FLAG_CENTER, true); m_layout = Hyprtoolkit::CColumnLayoutBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}})->gap(5)->commence(); m_topText = Hyprtoolkit::CTextBuilder::begin() ->text(std::string{g_ui->m_shutdownLabel}) ->color([] { return g_ui->backend()->getPalette()->m_colors.text; }) ->fontSize(Hyprtoolkit::CFontSize{Hyprtoolkit::CFontSize::HT_FONT_H1}) ->commence(); m_subText = Hyprtoolkit::CTextBuilder::begin() ->text("Waiting for your apps to exit.\nYou can force quit Hyprland, but that risks losing unsaved progress.") ->color([] { return g_ui->backend()->getPalette()->m_colors.text; }) ->fontSize(Hyprtoolkit::CFontSize{Hyprtoolkit::CFontSize::HT_FONT_TEXT}) ->commence(); m_spacer = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_ABSOLUTE, {1.F, 20.F}})->commence(); m_spacer2 = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_ABSOLUTE, {1.F, 20.F}})->commence(); m_appListNull = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_ABSOLUTE, {1.F, 1.F}})->commence(); m_appListNull->setGrow(true); m_appListRect = Hyprtoolkit::CRectangleBuilder::begin() ->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}}) ->color([] { return Hyprtoolkit::CHyprColor{0}; }) ->rounding(g_ui->backend()->getPalette()->m_vars.bigRounding) ->borderThickness(1) ->borderColor([] { return g_ui->backend()->getPalette()->m_colors.accent; }) ->commence(); m_appListScroll = Hyprtoolkit::CScrollAreaBuilder::begin() ->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, {1, 1}}) ->scrollY(true) ->scrollX(false) ->commence(); m_appListLayout = Hyprtoolkit::CColumnLayoutBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_AUTO, {1, 1}})->gap(8)->commence(); m_buttonLayout = Hyprtoolkit::CRowLayoutBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_PERCENT, Hyprtoolkit::CDynamicSize::HT_SIZE_AUTO, {1, 1}})->gap(5)->commence(); auto spacer3 = Hyprtoolkit::CNullBuilder::begin()->size({Hyprtoolkit::CDynamicSize::HT_SIZE_ABSOLUTE, Hyprtoolkit::CDynamicSize::HT_SIZE_ABSOLUTE, {1.F, 1.F}})->commence(); spacer3->setGrow(true, false); m_buttonLayout->addChild(spacer3); m_forceQuit = makeButton( "Force quit", [](auto) { State::state()->killAllApps(); g_ui->exit(true); }, 8.F); m_cancel = makeButton("Cancel", [](auto) { g_ui->exit(false); }, 8.F); m_buttonLayout->addChild(m_cancel); m_buttonLayout->addChild(m_forceQuit); m_window->m_rootElement->addChild(m_bg); m_window->m_rootElement->addChild(m_null); m_null->addChild(m_layout); m_appListNull->addChild(m_appListRect); m_appListRect->addChild(m_appListScroll); m_appListScroll->addChild(m_appListLayout); m_layout->addChild(m_topText); m_layout->addChild(m_subText); m_layout->addChild(m_spacer); m_layout->addChild(m_appListNull); m_layout->addChild(m_spacer2); m_layout->addChild(m_buttonLayout); update(); m_window->open(); } void CMonitorState::update() { m_apps.clear(); m_appListLayout->clearChildren(); const auto& APPS = State::state()->apps(); for (const auto& APP : APPS) { m_apps.emplace_back(makeUnique(APP->m_class, APP->m_title)); m_appListLayout->addChild(m_apps.back()->m_null); } } void CUI::registerOutput(const SP& mon) { m_states.emplace_back(makeUnique(mon)); mon->m_events.removed.listenStatic([this, m = WP{mon}] { std::erase_if(m_states, [&m](const auto& e) { return e->m_monitorName == m->port(); }); }); } void CUI::exit(bool closeHl) { g_ui->m_states.clear(); g_ui->backend()->addIdle([this, closeHl] { g_ui->m_backend->destroy(); g_ui->m_backend.reset(); if (closeHl && !m_noExit && !State::state()->m_dryRun) { //NOLINTNEXTLINE std::string cmd = State::state()->m_useLua ? "/dispatch hl.dsp.exit()" : "/dispatch exit"; HyprlandIPC::getFromSocket(cmd); if (m_postExitCmd) { CProcess proc("/bin/sh", {"-c", m_postExitCmd.value()}); proc.runAsync(); } } }); } void CUI::setTimer() { // every 5 seconds or so, attempt to sigterm apps again static uint16_t counter = 0; constexpr const uint16_t COUNTER_MAX = 30; m_updateTimer = m_backend->addTimer( std::chrono::milliseconds(150), [this](ASP timer, void* d) { if (State::state()->apps().empty()) { exit(true); return; } counter++; if (counter > COUNTER_MAX) { g_logger->log(LOG_DEBUG, "Re-closing apps"); counter = 0; State::state()->reexitApps(); } if (!State::state()->updateState()) { setTimer(); return; // no changes } for (const auto& s : m_states) { s->update(); } setTimer(); }, nullptr); } bool CUI::run() { auto data = Hyprtoolkit::IBackend::SBackendCreationData(); data.pLogConnection = makeShared(*g_logger); data.pLogConnection->setName("hyprtoolkit"); data.pLogConnection->setLogLevel(LOG_DEBUG); m_backend = Hyprtoolkit::IBackend::createWithData(data); if (!m_backend) return false; { const auto MONITORS = m_backend->getOutputs(); for (const auto& m : MONITORS) { registerOutput(m); } m_listeners.newMon = m_backend->m_events.outputAdded.listen([this](SP mon) { registerOutput(mon); }); g_logger->log(LOG_DEBUG, "Found {} output(s)", MONITORS.size()); setTimer(); } m_backend->enterLoop(); return true; } SP CUI::backend() { return m_backend; } hyprshutdown-0.1.1/src/ui/UI.hpp000066400000000000000000000053371520067112300165260ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include "../helpers/Memory.hpp" class CMonitorState { public: CMonitorState(SP output); ~CMonitorState() = default; CMonitorState(const CMonitorState&) = delete; CMonitorState(CMonitorState&) = delete; CMonitorState(CMonitorState&&) = delete; void update(); std::string m_monitorName; private: SP m_window; SP m_bg; SP m_null; SP m_spacer, m_spacer2; SP m_layout; SP m_topText; SP m_subText; SP m_buttonLayout; SP m_forceQuit, m_cancel; SP m_appListNull; SP m_appListRect; SP m_appListScroll; SP m_appListLayout; struct SAppListApp { SAppListApp(const std::string_view& clazz, const std::string_view& title); SP m_null, m_titleNull, m_classNull; SP m_layout; SP m_title; SP m_class; }; std::vector> m_apps; }; class CUI { public: CUI(); ~CUI(); bool run(); SP backend(); bool m_noExit = false; std::optional m_postExitCmd; std::string m_shutdownLabel; private: void registerOutput(const SP& mon); void setTimer(); void exit(bool closeHl = false); SP m_backend; ASP m_updateTimer; std::vector> m_states; struct { Hyprutils::Signal::CHyprSignalListener newMon; } m_listeners; friend class CMonitorState; }; inline UP g_ui;