pax_global_header00006660000000000000000000000064135205057350014517gustar00rootroot0000000000000052 comment=c726a05138e55c7247bd7e370aa21b1586d86ea5 notiffany-0.1.3/000077500000000000000000000000001352050573500135155ustar00rootroot00000000000000notiffany-0.1.3/.gitignore000066400000000000000000000001661352050573500155100ustar00rootroot00000000000000/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a mkmf.log notiffany-0.1.3/.rspec000066400000000000000000000000651352050573500146330ustar00rootroot00000000000000--format documentation --color --require spec_helper notiffany-0.1.3/.rubocop.yml000066400000000000000000000000401352050573500157610ustar00rootroot00000000000000inherit_from: .rubocop_todo.yml notiffany-0.1.3/.rubocop_todo.yml000066400000000000000000000063501352050573500170200ustar00rootroot00000000000000# This configuration was generated by # `rubocop --auto-gen-config` # on 2016-05-18 02:46:59 +0200 using RuboCop version 0.40.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 6 # Configuration parameters: CountComments. Metrics/ClassLength: Max: 485 # Offense count: 3 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 18 # Offense count: 5 # Configuration parameters: CountComments. Metrics/ModuleLength: Max: 334 # Offense count: 10 Style/Documentation: Exclude: - 'spec/**/*' - 'test/**/*' - 'lib/notiffany.rb' - 'lib/notiffany/notifier.rb' - 'lib/notiffany/notifier/base.rb' - 'lib/notiffany/notifier/detected.rb' - 'lib/notiffany/notifier/tmux.rb' # Offense count: 52 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: leading, trailing Style/DotPosition: Enabled: false # Offense count: 11 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: symmetrical, new_line, same_line Style/MultilineMethodCallBraceLayout: Exclude: - 'spec/lib/notiffany/notifier/growl_spec.rb' - 'spec/lib/notiffany/notifier/libnotify_spec.rb' - 'spec/lib/notiffany/notifier/rb_notifu_spec.rb' - 'spec/lib/notiffany/notifier/terminal_notifier_spec.rb' - 'spec/lib/notiffany/notifier/tmux_spec.rb' # Offense count: 25 # Cop supports --auto-correct. Style/MutableConstant: Exclude: - 'lib/notiffany/notifier.rb' - 'lib/notiffany/notifier/base.rb' - 'lib/notiffany/notifier/detected.rb' - 'lib/notiffany/notifier/file.rb' - 'lib/notiffany/notifier/gntp.rb' - 'lib/notiffany/notifier/growl.rb' - 'lib/notiffany/notifier/libnotify.rb' - 'lib/notiffany/notifier/notifysend.rb' - 'lib/notiffany/notifier/rb_notifu.rb' - 'lib/notiffany/notifier/terminal_notifier.rb' - 'lib/notiffany/notifier/terminal_title.rb' - 'lib/notiffany/notifier/tmux.rb' - 'lib/notiffany/version.rb' - 'spec/lib/notiffany/notifier/base_spec.rb' # Offense count: 23 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: only_raise, only_fail, semantic Style/SignalException: Exclude: - 'lib/notiffany/notifier.rb' - 'lib/notiffany/notifier/base.rb' - 'lib/notiffany/notifier/detected.rb' - 'lib/notiffany/notifier/file.rb' - 'lib/notiffany/notifier/growl.rb' - 'lib/notiffany/notifier/notifysend.rb' - 'lib/notiffany/notifier/terminal_notifier.rb' - 'lib/notiffany/notifier/tmux.rb' - 'spec/lib/notiffany/notifier/detected_spec.rb' # Offense count: 13 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: use_perl_names, use_english_names Style/SpecialGlobalVars: Enabled: false # Offense count: 967 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes Style/StringLiterals: Enabled: false notiffany-0.1.3/.travis.yml000066400000000000000000000006431352050573500156310ustar00rootroot00000000000000language: ruby bundler_args: --without development rvm: - ruby-head - 2.3.8 - 2.4.6 - 2.5.5 - 2.6.3 - jruby-9.1.17.0 sudo: false cache: bundler before_install: - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true - gem uninstall -v '>= 2' -ax bundler || true # for jruby - gem install bundler -v '~> 1.7' matrix: allow_failures: - rvm: ruby-head # ruby-head bundles bundler 2.1 notiffany-0.1.3/Gemfile000066400000000000000000000006271352050573500150150ustar00rootroot00000000000000source 'https://rubygems.org' # Specify your gem's dependencies in notiffany.gemspec gemspec development_group: :gem_build_tools gem "rake", "~> 11.1" gem 'nenv', "~> 0.3" group :test do gem "rspec", "~> 3.4" end group :development do gem 'guard-rspec', "~> 4.6", require: false gem 'listen', "~> 3.1" gem 'guard-rubocop', "~> 1.2", require: false gem 'rubocop', '~> 0.40', require: false end notiffany-0.1.3/Guardfile000066400000000000000000000037371352050573500153540ustar00rootroot00000000000000# A sample Guardfile # More info at https://github.com/guard/guard#readme ## Uncomment and set this to only include directories you want to watch # directories %w(app lib config test spec feature) ## Uncomment to clear the screen before every task # clearing :on ## Make Guard exit when config is changed so it can be restarted # ## Note: if you want Guard to automatically start up again, run guard in a ## shell loop, e.g.: # # $ while bundle exec guard; do echo "Restarting Guard..."; done # ## Note: if you are using the `directories` clause above and you are not ## watching the project directory ('.'), the you will want to move the Guardfile ## to a watched dir and symlink it back, e.g. # # $ mkdir config # $ mv Guardfile config/ # $ ln -s config/Guardfile . # # and, you'll have to watch "config/Guardfile" instead of "Guardfile" # watch("Guardfile") do UI.info "Exiting because Guard must be restarted for changes to take effect" exit 0 end # Note: The cmd option is now required due to the increasing number of ways # rspec may be run, below are examples of the most common uses. # * bundler: 'bundle exec rspec' # * bundler binstubs: 'bin/rspec' # * spring: 'bin/rspec' (This will use spring if running and you have # installed the spring binstubs per the docs) # * zeus: 'zeus rspec' (requires the server to be started separately) # * 'just' rspec: 'rspec' group :specs, halt_on_fail: true do guard :rspec, cmd: "bundle exec rspec" do require "guard/rspec/dsl" dsl = Guard::RSpec::Dsl.new(self) # Feel free to open issues for suggestions and improvements # RSpec files rspec = dsl.rspec watch(rspec.spec_helper) { rspec.spec_dir } watch(rspec.spec_support) { rspec.spec_dir } watch(rspec.spec_files) # Ruby files ruby = dsl.ruby dsl.watch_spec_files_for(ruby.lib_files) end guard :rubocop do watch(/.+\.rb$/) watch(%r{(?:.+/)?\.rubocop(?:_todo)\.yml$}) { |m| File.dirname(m[0]) } end end notiffany-0.1.3/LICENSE.txt000066400000000000000000000020601352050573500153360ustar00rootroot00000000000000Copyright (c) 2014 Cezary Baginski MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. notiffany-0.1.3/README.md000066400000000000000000000045651352050573500150060ustar00rootroot00000000000000# Notiffany Notification library supporting popular notifiers, such as: - Growl - libnotify - TMux - Emacs (see: https://github.com/guard/notiffany/wiki/Emacs-support) - rb-notifu - notifysend - gntp - TerminalNotifier ## Features - most popular notification libraries supported - easy to override options at any level (new(), notify()) - using multiple notifiers simultaneously - child processes reuse same configuration ## Installation Add this line to your application's Gemfile: ```ruby gem 'notiffany' ``` And then execute: $ bundle Or install it yourself as: $ gem install notiffany ## Usage Basic notification ```ruby notifier = Notiffany.connect(title: "A message") notifier.notify("Hello there!", image: :success) notifier.disconnect # some plugins like TMux and TerminalTitle rely on this ``` Enabling/disabling and on/off ### disable with option ```ruby notifier = Notiffany.connect(notify: false) notifier.notify('hello') # does nothing ``` ### switch on/off using methods ```ruby notifier = Notiffany.connect notifier.turn_off notifier.turn_on notifier.toggle ``` ### Customizing options Options vary on the notifier type. The full list is here: https://github.com/guard/notiffany/tree/master/lib/notiffany/notifier Currently, only TMux has "dynamic options". (Open an issue if you need this for other plugins). "Dynamic options" means that you can have custom options (and custom defaults) for custom notifications. Currently, the main notification types are: `success`, `pending`, `failed` and `notify` For example, the default message format for TMux is: `default_message_format: "%s - %s"` If you send a notification `success`, it will look for `success_message_format` and if that setting isn't available, it will fall back to `default_message_format`. This means you can set colors for any notification type, e.g. you can set `foo_message_color`, for notifications of type `foo`. Ideally in the future this would allow you to send custom notifications with custom icons, e.g. `foo_icon` which has a default value of `default_icon` for plugins that show icons, etc. ## Contributing 1. Fork it ( https://github.com/[my-github-username]/notiffany/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request notiffany-0.1.3/Rakefile000066400000000000000000000003351352050573500151630ustar00rootroot00000000000000require "bundler/gem_tasks" require 'nenv' default_tasks = [] require 'rspec/core/rake_task' default_tasks << RSpec::Core::RakeTask.new(:spec) do |t| t.verbose = Nenv.ci? end task default: default_tasks.map(&:name) notiffany-0.1.3/images/000077500000000000000000000000001352050573500147625ustar00rootroot00000000000000notiffany-0.1.3/images/failed.png000077500000000000000000000031371352050573500167230ustar00rootroot00000000000000PNG  IHDRkXT`PLTELiqnI餌[.pHU+W,ʛY-V,ɞӒ|U+pIrNrPoL٠b:]3sMwQpNc:pKÿ)@1tRNS@fIDATxWR#A[,B,7~#3/3؟%XT?m×!5uMn|VZ uXk,ŗ_`/_vE|VR eXK,_`Ͻ>w>|^\ yxs,ŧ_OvG>.O`>۩T 泵*OOÇ[:8}>v>nA>fA >|A>tA>lA$>dA,~hm "[/ʯ]_ 2fAl 2/Z߿ oA.l 2/._? _]<`.X4 VRJR2xO[?i OW?YTzOS`?IF_`?x [?hhW?X @a @(PyH w[iBH=z_,=KI\O./}P~nm\@n)mX@^ikmT@N-+mP@׿/ʿ4v_6 X*`_% X.`_E6 X*`_e X.`ߟ> x*ǟ' x.ϟG> x*ןgc )^ mn)˿@4 Do)ӿ@Tq D)@t d+@y d+@6 d,@VyNm <.XV-W0ֳ+٩hr~C _kK| .ȗ\`/*_j]| .\Ϸ k>O|.mO^GӤ/) &,IW8YS׉ ?ORq} ϸ'P_c @ /@u\9`g@Vt!h C Pj.@^Rcb ๿P,U`-y]{k p,?]Df'G 9R@"* y= H+ _@Ma _й}Q@*P/y~?/e<*X_p??fCLL^IENDB`notiffany-0.1.3/images/guard.png000066400000000000000000004725621352050573500166120ustar00rootroot00000000000000PNG  IHDR~LQ5 IDATx[sdyv[ ; F\H䈦Dɦp(a?GSٟo[vX!h^$ҲHQx.==(T"qd".3(ϊ*$7uk ,X` ,XA[`OuE Y@,oR߱ ~L-X'U KI|s} ?xⷢ/5 |KnDKJL0_ƀ쀯mV1BnM+O g , i#PL:zAn-<\M|ȷ ]`OR#΁oYBQ+;~=zI+u^~,X҂zfRA;7 31="Q$ďs(u{B@1#QU ` oy;^i Ґ*ar07VL_`BH dǑAb؋e&sPQh[,XijTU8^JMD$ә>YS kddVPEG#)&S Uj;`\D<ȝ/[Q"ވ6cK+Z]Jx BZgj)nC1a`qL8U/y#; YP]5`,ML ^AA?XyKkDy܊VfLk/Ĵ`! Ro]Ұک&v7yhVK?fwy`(C w.ō.ʪ_nbqG$E1ЛZJx BZg"hs?ĝ(fl*gB ]A}*#}&)i[gy$EaD7k=~>wbA|61Fč\bۅ2z= rJLZc;o !-X/CB XH0L"bs(HPrPij}Ri\.4'(NгI*wyLO5 o,`d-o01fKvQYP@ F;*[Bެ.44i7Py6L;%2E_s܈LL0jN?DwdsT?g.9|%qyBH  ~C'Az e/BLSx={l=d }s %Q9Ceend k`ݙ"N\w]If~|j) \PsNEhҟ\Vz~3L?ت\} BZ#DB)&wOSF! P'{,!Ǯe EGSV%<|_\7@1VX&e3qEiPz repj ZQq1*sD:W9li*V3`! ~LVSP.EF`#i3AClj@9XcpTT |:mGфQ:QB* `suci3E_:?mqvnŷJ菑{vtoSwOɸp Cqdz=DoJg\^̥>n740 hQH BZ𳉿ߵu2 n~󛬸z }!5p4"[?HD2j~bN< UH`5/0CX Lh"`H 1Fk![;wo$>OBy#A9ksG 2aP6'#؛Oa^/(?$?ǿkه`! |~Wٿ кx!}vxo=ᠨ{gPz[Ôu6ț] 1P 8j2+Ұ)Iap9°rۥvC[k{ؽjgJ ފfH(Oy O0;̼Jw 97ߥ9Fb|}1LOp^} BZ3Ic`3ľrWĿkΙ F_q jCg> :ӵ"Et=08 P0Ţw]*H)vMKG(5t\4Nj& iZPLMЅ!H Pʄ։ăGgSGO{}ÄΫ`[ ds^qS# nHVI< mbIٯ&x :HLB-O＀(,XH=-X҂`^Cnŀ1ଭ`mUAlyxO0܊ A4sRз'X;q9*;_IWQ|sotOdw׫So~oʷRwx]4OzC4E@E\XU4SD 8}]v)'຋2~T\%k"$Eo{ۧ_u)\1Lr ? ]5"u)0~ۜ4%"Q~$ogӰ s)@3<7c19ǂ !- ooTFv vqw"@%*ֆC1}MIDxȱ,mg٠xFxXڅM;K]Nj]vJnjM&@XCPBLSn올\&L1(vrϖ@~ߛe[SߜWJþ۰'bʾվ:E->' gy.u.׵za!viT ] )v]R)7{{),0MY0vq D\X*9D,{[SefLvks]T4ύWݯwU}_U#{!.Gi]#L= DŽz湢X҂ܥCٝ'n- Vh8EmgPHl+ #|J~@0~ǔ[\5POOϺOCw")_(V ڀH,aNI X)g.*2ڮW`d f' ( C"6WS^̚6>3(#dC .JA<""Րr/.=RzZ說K"-lN,`,yدVSx wbW}h(+٭3W`!x7!nd>McTϐ_OoE`BH ~b|I 5LHjC̾)2oK'*}Xĺ$U)*]_~d^/["CTP՝_~K܀+Cmx\L%S&2Mfeo5;7y^3uDlZzfW^[YA{^9E|$v|qKS:ئ9 tlVŐSz0 m*NQGGlF"7y1[ef7)KAq1yk?ksj6wPO[޷M~9B[3j Âo og&.PVQU2+q$Ew$PiF˽:!ͫk6Y[|;(AWm6 do]z䭡C2CU4H}.*ytR5q-nede&99N.#S%TWm}a*" YaFCsjhhk$୭v1״(UHZQ"BsbO8TM}F6k ]MU_ަ(D]97)w.gm^Xc;_NtԂ,h%*7j^<=pC35|ںgͼITPFDA3⣄$ ࢳ?N\ ^P.`졾e +%O&oUVF\킏:`Eԩ?LkZw1.[kνА04o&Fbu!N"TL.J|(JHEv59NPۛ;Cڵ;GDa|ENBE&Z;t9AV8v+wMNiG ۳)[-FD}5Vgk(k)n^ʧo"* BZ/h?$y X͇L$-X[pKKɹCc(C鯄c}3̸(g6y]>!/V0=]諵 ic)Ԉ'SuטU8l^p+:MI.t11Y;Q2Myd-P5ZBhɶ54*9$uNs@⠞1frI:fKmfqP3c¡YIуrro-fbê _ߘmgA&*Bc$uȬ uO zͽv! !-0[Cq+ 8Pi G8m|s3#k;oE}fo/|OQ?=P sx_zo6LMḊ\puq5E\UåД DIʯ&<fI;r.ǍsPjwػs!kf5d6!EKnnà0`oqhGb.Fj;l8.൥~ʬY+Id.ΔwY2׍! I~mAcv-oa3 9ZtYj -Xiq9yGS7A()er7-t@o jqatVruENl_)SYMjj-tvehcE[ v8=f(Z63kmY8bV4#R|$fztċvc)yQ}I^Q4߸`VS[d;ʜI9lԙ<;y8jq$'947$м K¼N}>z~j=/ƐܿJ=Rx͈=È@~sq-Xi83^";ZMg Ewyʏp5؟=]_$z^t}264ՔmaJnmڤ!lpIU0u]9%E(6nRC }ԔPΏ51E$2j5$<綕4͎Dx)Cm-8,MSf[I2P($Et|K_kVZgVckZzE5OA}.&Q'zlmFZ/xH !-8? *LЙfan sߨz`x35azasi6ƛ;Qf?6r Nu>l:.LQ]>ke9Gun/PODԊV-8<ר#5E:U*MM^뛉(!w 1-Xi c^bQ~rxŃHښ_\ц{VsI=C*^"[1W#|B0O ih76 e5M%"6!ϧkhB-I;ULgӺٮ;٣=TLNy8=2=JlR8}[zK9jL} =SWBHL8&A }R1Xɯ޷^?I_[.w7+(w`}y+N+c#Ժ]/USLR6M@5V6k$& oʜ֒R 5S3@ gft퐈`tb{!:R60gSO樜ޡ$u|rN?qtG 螤8<"?s5⢓2$)vd\ۄQA.(*m@G7NP/\~[ga__BZWAPڡ37t:-nd1Í`V< 1& zd0.Ӭ2㘀*)j)h&u5tb=hdup\k{ͼhzu]I\&[)Ja{Չ{}Ȏp8mW:Ou{ZOVM:Gdijje7gqyHyJ4v;Fft8(D|Q*m~`dx08UNx>Up}Y}s~9E/]=]R.2>Kn%ҧ~FTV3vAs$2sI)YwOھ6[#}!?#8692`(#_Ԗ}U )+'j{ۆ]g{Fj=}rXn=RCmQM*-ڸEb.qi q‚qҕh:sv:j>]yz(2(|c爢:Im`u?W7?8}~?ྋjb>LUNoRb ̫.t];EJ`2ŶtWؾWW^(k@y}a}Qo.lƙRhQ:|{a. !-*FoqŸދ^lf+w13 }]\Sc}#vAmGP_Ck裐MɨvKn:Jzi̩>fNO8N8޵vZG%a'<9OTpBFߩwh=yd85(T_Љ9>C@ASIj~RƱʨl1;Ĝܐ)%rMA^eeh1۰,g(mpsI6:.rG+ Y!U/.ǣ 7{6.C.PVzms&*/ŐbB 4J#doRHois fVH5$Gs}4&ļ Q%NcHT"HPŜy|4jޭh VZɸ F'n_[嬞UuRe<,e mn -NhN('3}m=..cg'*ָw&&>m};?_"voWϯI|1=UD؆zqe^V+wzһqY]~BZ+{<=M1";f 3)*@<|.kw)]jqmT3#6.DW7pމMUDܵYhE+ɍGT) -QP~NC3q9."H(hax#byW-֣-D\wPں2.:CC|Ayz+'psEv\dc}Rp)GxpNM~[ǯݩʚO#koX:=Լ E癄bR5̭xf̼(0;t ٝ2/sC|nu5+)w;D:3ܛqm*7^^FcQH ~ W >d]"kKa /:9Ee'[ߊp6'i-m 9ޭL}/C;%M/;Yq9q_9=<|0:M1٣yc|}BC'dwwDL/;N1/;Cs,|\{Qw:'xj:|TN O= WS $nJeע&".3t˼0<c*Gܓ%8&})4Jc=pQ>'/CO&IAH8|袛_Z?&^멂:)4 Nqo;" OULL'F7汦1mJowǰw2cm=1/{l//rfYC$vqZwv:56idtVr<"瘽743P{^Pq;=y)3CfQH ~)=Gvs??yC݇xvW9׈O9U\7/}{|wmvwvNq5urĥIzLLt1zx/9q龡'n=)u7@]ăS}4dN{/>!G < =h'sO4EM re{E}pN~^̏kQ8'yBqgkZ#4.R M qf]yC%"QyzGId;I;yfjgqy)In.úӷ0>>|dh/wj5'Tp&nBH |WCwFsnm ĺ9k먇=D?_vveYaFÔ[ȻN_`GH}>ait2p(kNsu8s3I'6|x!s*w;NKEw7=R\:4g/\ϫ[=N|_r2ia3ƙv@f_[귊!Bmmkq0ʮ27q~-^ߠbxnF;/ Gȝ y-?!YE%>}rLgjľo"96@`W*^FBZc7bדUU^]sDZ17Wv@՞z o/Y:s(v-}A~\->̤ aIf2:aWA_rmYm%Tܓl ǵ7=ڟQPiPL'j݉jʓOcxd8ly9ꬽ!m'<=&_4x1 ; s3ZZ#- ]wAbTݔϨizIzIә-?ºs@f~RnspvmͱUm5"#)Zhg=^ܿRZiObA5QX {\GҐǀ`#m~)(>Zm5yy]QE0;>Nlܜ 'r>10/ĽYǑ7LV]:D 6꺩C|û5aI);|![迋xc'7eB?>qՖB92^#l7_ӯc}^ZQ:E/idgxÉ#QD3P7Y%oTk~wB>Ya@0ՇƉs9Qz*|g˜ωpN {K<* =f:WCS-+N tTzؔ*sETAN?1dQ\]Nmg*Ȣ]5k2>[To1^8xmƷΖ\c:E- Nإ}LBftfNTS9ܩO'Asok:r:{%v0a*)=ꫝ!Z!t~N^jS9˰ݶ軎f?5Aͯivͭf`%p(:h'-m6[WF6O~2 0_'y2?`CSDIeXnuUV9{q"0#WȌ<^ MI;Ƅk>1A TD `ȂĻnW>/݃g0`QƩ~B%|5hf>` `AH LPp5vs5`y.ٶ-5X˯ѓjΞ㡰 y/Q~_s#=hȵ m/ s1_ #VEG'yQ 4{h\ 6 w%}B5 V}kNPb¢6 V[`qh2Jl',M;Pt,m+)͢MV^hkX !/>o)"wfժƱ$h^:kF0 ٧'u xLus*vH ќG7p ćKc,d &?-t,̻ :k+73hCK[8 M "ڋ<!9#D/,]7棋ٶ… tΧwhD~wסYiEŸ1'N zn&fUDo5^gT][4Χx߬"uXP>oP\V(X7 z#ԟk'T4]k7[KniZ&9FGN~ M 8B8Fi"(L4x-EG~ n [Fa?wʺ [A\V?9p#/B!X_cR/PGO'x)H1:7d_[ P&PB#1:02@yv"_yaP BrrąKSYSx+`00mwH-lvUr;ۮG;qB[tJ-]bu>12$_jڵŶMFS=ϡh}e&tFo"mӮy6/ BZ'ή٤MkNMgۥ01UѽR1^VPU2o0ɐ '%GekKq?w fLָ2[c$$]Px׹]$%f :?G᳃Ip9ps W0;?I5r]e r{م]> :dp QR?JU"_'5шdT:H% Mn \qŠzbfOЅ]uěk97@]Vuۄrv( n+ AZw|N}J~6ti#؊nwgьNYPdrn:yVΦj>! ƹw)vwO+6NWY3pDLd7-@_1}g `? <v?V 2O!Uȿ_үK, +п챒Tܼ \f`<ԌTL10@:2++F8b T9'nm#OGC]QZv"E ŷs^qE]kFS]ABu{&.cρM묏Y{Ŵ .}~fEv6sFu\3HK5\**Τ 9OQg(jRqJ, (d0&'OYD&Gx=L?ٲ/J9CF|9^ үq'Crxq1N-0C1!:82x>V>d)xK#@4Z,]HN[";.dum?鬪fCȫ;qIP]r4TyoQa*6@jfǙgscIV_3Me}0zM*׻['7o:$]5GNPWq튀&qx5YT5i\*Z*VyPro+,m kkm6Tpa-(l--ԷBg$Wڻ(-"hbAj'j @ri2F'MȏMx-GLЭ;A '30 , #vT/ү!=G2w cc8|| B =x%bCӅp3!똉xK~+Wi;Q>ۣY.I`64®Zf b)ruq݈Eccv;RmE‘Ew܍S}Y=rnij+DAGL?)AXtUsU#.Skj)sQ~dw$.c? _)&Ϟ-jQV>ͯ<IZ` 86vJJm0o^GVkOvf!frN>2k޳d'&(J=ԯy[|WYw]JGxҟWCv ܾv%A ǂk8,j/W ~]Dp 寀C`%u(L4 v <x- ot 1]M fC-HTI V hSVM馞Q5PL8upS=U;-g^Az,u~Ғd5Iwsz kC?_>.1u\Pn 7^ {36SfC&52ݣ9\8 jn\\j}圡Cyh㴚{8c+J}7[3 {>$wp8@]"&dD*  (ہL]4sF qWkY>/VB/W#LQN‡@;q9=*j9?Ɵ51;6;g;<G|KhOWJd!y@JDϋe[u&-C첋bGݼlf$=_22ΫÀ8 VZs6"AUv ,;]]c'mPZm ;7߿9K \@/,کG7r}%7k-BdOLI s *x&H8iEA)c,@mpr) NLςg_~JܟAw>'0Q(KrŽ@7. \X>3;c8NeLP܊́h| W@^DRw$N0EBHZX#eUR,AG$/37`6C.$gU5s:Cf!Ȃn|u V5[Ş+X+ i~4[O)F/13m8vtɪ?붌 NYq/ȺLCgQJ) A:{_ wJؿ𑗬:10oR1C☈}? Ưu ~u.#RpX:r4+ Ir>vhǻECi\Pq7_bikzp݁'c 7eTR~Rw= n@vϫ 8fT0g 'p5q2^d`BWS%9`d4Qvj> i/rR uxW$0wxE76;Vq֟Vܖ |v.F]@\Y}ٹmݾ9#6JoDђT8JSH z_A(W=RV{ֵ_{ڣՋ:'I˲t{Phl0䑐hMy^uwO"8&#C v!`&NE!8ؕ8%' ^ /Oʚ8 ?>^Ct x!Uԧgb_2`Uz4hrr9a.ZОK 1 3Z%ͽ #zbj>hVyCUa]xBy&ᅓ fD[Ϝuܵu۩ NТde2y8A JBǚ 5$_ڃ]Q^"%xa!VȻE kԛaOU~^E N.׹+|5^ ݽQ֛-:{XEʡwzWgZHC«JI7%9 L%h;1{F&]CHV6^:q"A05 ]$M#r?cQ}&G1$@2 +rZXNk1 ՌQu/X ,:rBJ^Vg+FD)wן@;v 糃җ <gMّ!*>7|Wg$aGbm7gm1LkYEAJ>G|[僲q j{l8ŭ5b`v,{b)xMn3"0+r0lNdy`ff>qpq%j Iw&xQ 7i b3ߒŕ%|mx)H//x(1c&ϧn6& b-S>AW a?Ui?LMҩCD뱅3j'^0cU\[`ֶֻ(BތBM|eCT%9^?ʱA$ΊtUqVu͋*}E0C T!U㺚*ʢ*BZM*83j~F\R}ܓ P)U^Ny"U"l|PϷ_eƈǚ#*6E!EePKRmb~V gCG> qWA+M-.Bv4 L."p Ѓ!K~|)"zCɘ31uJLz)H//8l%@th8V~2 W86C"C 21R8q(bD6q# C^,xٹ:v[k3{d9%>uRBDP:tsPOFCژ~gv[>C;g9lzdy.nbkEg枚Ok+bk&XtZ γ6m>xY*x*8?L*ִߊ*V_kѾ*ƱsDkV)fYFPŌ,JLP((huqo@z!MF==IPq\y]#*7x%;>s%vxҁ8+q%krHE9_y7XrZ$;\c2xը4T1u Wi;2p!~]9.cQLYڅ`$1qe EWwsCK[o9咃sff#98#pyvw<WFFhz_J݇q 4!!C TFnW̍7bb^W;j=Z4 ZօzrCɺhHڟ0ه)BURwϕҪ,Ґ~ƫъSE waBrF?-$\ fr.9#pfc=a+zGKK>1ǽ>w=yh8%ϾHܯA`/#oL_k>!߱p @&,CvƉҬy7xq4 '`/D3rd@\PLQD gFm0Ɣ`ZblrWU)m4׈yZ r-qKX-MvgV~C[nq)$xYncǟ<<ʢY)9wu)hpb8zl E(l}A)\qg(wfejO4i%iN$ofZHnq}/^=p ^?McqH^ wr<jaz!%ґ` g-0'1_?oq!rO;;tS 'pw86L70xf& C:@scé y:&irٰ iGI_A ;voOE!HƁz}mN{)H/iQʵ #ayiGs$?)1kx!h_>J`4^:8<%9E `d9u)M!\zɈS1ڧΧnm1YNw}U23L(:^WicGa{~;3UWPkUۈٳL 0VB7ڤ"MZ^fkeǪἅauc1CX? *r~9' >(k`JYM*XVѓ`X23%(d69eޜIw{gp;{"BIKiGErzҪ|'T v-d'.Kؿ|{ 8~亞17'cO.8(X'y(y2!F|#gfW%f&rml,,"/B\-zZOs]][SUYneۋMGKD=v-+_\Lթw׆V0t hJoXBl&]]6$>y7ܑD0+G9!ymaqT? QuVPh:8הKz)H_TESu2؇g vEE7"!ٙtp8>x՝WI.a AX2`pg2M&cY}Ҧ7ߢgWHIɓ-v>sΧ.%o>)<65|Q?"_.ډaf[ D}Fn'͹;HDA-xϫ UgGE!-HK(e IDATz^JkGŪ*v{)lZG}F#Wms3g֯Cf2ŠB=ۅ04rAI2w1ӯ3q gk;CǻG {8&r`K$Oq*tqx)HlSYj gnPJ"&uE˥}j'&"~a<Qo#'v8LЅ7r"BDP`fy^ܐ*iy>=?@b%fxĔs ֍mctkI{W7ђOqz)H/A}X>Q?.S؃K8A 0L>/il߼$$Gq%#-N  0VJ ^PԀ/ *\Ʀ B3g8'_8]qlY0v(-1m@`KYܓxNݯ%#} !I8(7X@;}JNjɛi\_^ 7 vq -pLgC7O  d wep~y8gMgjMG&J'Ǘr͏;6s!{GM3qt<ӎ qx0\LU6# GI{pu Tj \+JHFC jps2=KgK1S4S=ipZZ g>t8ɭb;Y8$޽zz8+v5w_%}Z Hupr*?R/6:>]捊mbПF1bWZmu*U%EFf:4sm}(AJ"\Ξ']ů5=?O>p@t}"%:&Wm|-w&]0hR1^1Š/ kkm\",2"JDH>M:K䗂G7Q˩+HWIszl^yNt|Q?A1k@5Zmk9*x])%d)_a pǓ );&pOD۳䟚94}!ߡ&M/] r{wȘ ?p(kxx .00EGAT9⧵G C9IIb7P#GE=:EϴrY턻#9-z4kEܦXY,-qVm]zFV-\Y4A.gq(ϟpPifk[n'N8a=Ŗ6yNM:ܺ%)X6=ΫXsA= hWVRbO-^7;+"gs--ڞ͜XS{}FU!h.Y{S2#Nn-;D\e$sɏM:vY(SIm;A>1DH)8nyңߡt^~^ /Oᄀ9'"&,׎ۯ >|s"/3yI>LT80YQ 0JnDAZ ]J,2I>%_}jc=y܉Էv̞}ܰv[z\Ԃ{^HKˬ "fҿq38@kjԅbƱ 0{:#<;VC3:DZl긯3:b8ơsy]Xpe/b=V:mCOn;̊e?:i2H1||_>>(<@  {" tN>Ix.FN¯E O5[ 7A!JuL , R,Bi'rb+a=ՅNpR*i{- wY{$iôa0ۢf6Oߢ:_y _p+ ['цox zBAMlqڼU3vXV5QOlE`ȍ~J v`:"쐬5i R=g~ \[pȣ8r!Bk[]Zʠu&9~ 99Bg`b 4D F(& f0 Sc "B$B FR^l1F.ܼ507GU.{BeTf;`|E3Oq}̺, ݟ6J7٥K$ #(Ț7U4`1/M.f0F,Χp S5^\FƗ K<:Its3h5$ZBST-ւm&kּU:dh31MD1TԅҡrbL1'.4o3˔Ir܅o^73~qHh@ٕ['A'+%_cc&{g~D 58`< dѤ[4s:ѸwrS^?^^#D[I(rʻ#t+rrw[,'n .s9rӲڦ@[<Wx -_%> OSfE^^g6! [k.Xc4ED6(;'؆ M. Xei/z}KXTH5:Mq\-O:"؅\SvO-rM,ށ$Eb DabL;M $$W)Gou|)H/y~,}EaG.31ɂ0E?޽A7"`ހ8 8 9GF͑E&^?O%xbM۶r}ޗݎ9ͱ Zʛb+ >6sD3fͶPjTWtp=,Nxa?6ŷ}Z7y֙Kx!+.(H'gdʨϑ۔ܸ_؍g|~cݺSkj>tV|{.kنhB$L~+ Z7z=Rg(M>?M8 n9Q~FԠf_3T-1Pqtr w$k!C#3/<91iՉu<.D^eQ J"2;XfWB2IQ0dzlW!~÷@0;jUʇ)Ϛ)"6/w[#CN1e]b,ω7oeggW{g! aA7aN z#€xƂeC/ & ,irHÞfOMuUV֭JgH63}č8)m$/ҋ3=O,^0"+u5] M^~J z_?>V=\T`5ڼ|EջG]jNhY'FS h{Of\"uxY Nŏj؎Sx Sjafg~oQQB,ĔfH9<ğ jˬ=ة&@gp\^\)nv+y^ yÍ}U)zU@\ԉG0ƩH:PYie%50̦KT+:hqJ Cqc0Nu% !UI^" F7@7Eśj/߂lߜmvgx:Sca;۹l XpRLt{:0rYWG H{Q]J3m5pګiXmg]Dy5װfi8ru^ҿүZw{pl.\7u3^7buݫ@e=b(YԢT3;BIP\V0eB;2㸧w#A N;]B705⢞&v^V>+]h|I I 7Lmq껀 j2Oj& G1KPDF)YJfl 3p5]TEKPV47Gy23X|"\ԭV*, rEh5މ6gY~A&~oV?݅`^jlEk5 5hqa[:֓ږ2*nI@T PW4ĨKILYkdC}nוTqR7Z(;C(d)NۓՐ5pV λYפ痰GWX}؅}N\ 8SxvG’$au;M!#,b;kD^!DڍDUQMo0 0@ً8L*i]/fx0U.VՒ`h Ԙtw0U3!Z`P)dfS!@S'\tGj(sgRcud(vb7ub9'mI\iElaLnn0bBt#pTjfX=ZP_zxRŊzagsĚ޳m}Q#X^MuCԡ)cdgۉ}]4[5Bl͛:,q sc ^1qDӒɪksǭK\R;+Ixm&Ppe*T7uQ̈́N&4. л"k .ʨ?LBSSNuڐEpp Oc"A.``]셙i*?K pdj'#<y(1immGhcщ_&4l(vLwmx=c[dy=pQ' _r}J" B"W]7CWܻkۛs'uSf+= 62|^.cDQ梗Qΰڻ!o%/upB-.hɟZGZlcOX}=kה\_ +tNEx^?Kf&, d21< l,S fR@^[{f=lک y>l@;R`r$OJn6]n*l!!=ؑ QT7ZS/E12c"#GS.c? 6֮ΪU])p2{"{&,}5XVvg9'ds_ -n$&7Y}RbgXֹaXlibPC񤞇lםmm񚐋oZAŅw-pvV;rLV!هm۟gi? qm6|z᦭쁅ku u,t{$K,6⎥W'DDU5jɽb"#ծ_7{y(KFQrN7a&.ALD], !8?ڟF_OCz8?&{! ŏl+H IDAT}dɏ Sfeah8%Uf$UG2 $bH`61q3Wyj@-DvT bYwbW06DzE3tS_Ֆ6HqIkى|,\fZ+ݫxV' <6Ts$؎2J7u#eq^êkpQU7 6,9g #FbhU*oulqTvT7"kWVqo9VpvF)+)al#bM9 0LfT"BN,]RN;4%#NBYR0f?51 =LR7-=?wX_!G4#J. x?  {>O#L@Ox'؛2J`6ebLviv'],= ZȄS#+LdlDkSonYrȺ>2\-nB+SUw.5Ō0AzUV;XG~\)۱eY 5ރ~Zff:vıZ7{\y {d5'њ+.t8gS7GZy֎]=F~sf \ܸ\"IP҈WIH0 %8H[x"y}G^#F9q&GEYDP\hJ"0! /0s{(HZzk-L^2EI+~FˬSn}{ l J:Eɪ]#D{tM"{"n٤AM%ɦ"0tOw޿$>fn_ULª}^itMg=F%>κF0/{*nݏ7[ ]SsXhEONfXt-ytw6xRgƧ:+u4XEV@65*dfrB*".XԹ&8W]RWQM43GNY q]Q*Ezg@GwhBT hD4/f}\05'Dq_Pc hElX59l'UuUbP;΅pȚ bp5)4B $Kub"L v;G@0a P ck-6鮜=- 懂7Rȡx# wjKؓSSN)D GDCJf)6! ᤀ<ṕ\R톅WJ^9F /*?GVMKhf1|Ⱓ:H 5 %՜4blҧ&x/>vFL mW%5!Zqn#$[†5ZZ1*:6caqtq"}6=K7HV+ツמ:]1HbʩR鹴Ww9cFxjN ."*)j¤Y4‘!4p 4 [YDdfnixg{igF"p1p:&|~)F߆JUH ቯ =,h~&|=~ 5B[KP>E>|U B" 0)wBAƒW) ´w0j퉮;[kҁl Krnm55;;}E:^MGqߨ.\Y XO^Frׄ [\gZ籥Ʃ+njU0i-؜π\yNC"۸XxiU;1Y,m{}oηy:"%I%؏ O]^p@8m`yk0f97Jb. a 7 0)OBpU]++/aTP>IDhLU޻RsՍyma,`q1)M`<`8 w`|-IdwS8yl/0ų$(D&#NUWbh1܍ݰWL" FX660+Oմnb,m{{\TA p!Z'Za>룳FȨxxmQZ@;ޝ}t|A/thcN8Fm 3XHݰd-PqsQ×z vr^F"qsHi*#K 1̴>P8lszՓ{)+ S<ǜ% t}"0; `KkPA;vox~R %V4^! ҞxVn?)B%ob!+X;$ $RL[~ TMlF+$EXjs>~V`ǁ-^a}&mW#PBuCV ,Y9tWmmԵqjqbkÎ qjW# khx6rz;i>סoC7xiF>^ 5(k6r_< q̺GvgĮ/n;jQuhm:TkRl)餵[+ s}Ljb1'Q`PS}85~4=FMjDIƙLO3JJ&?@:CA#:*:#׀i5O͒R 0/EiCш1q0WSCp,̓+H8pnUQU 1 uW OYO'$]f w΂uDA[qq#׏y P.^mp%+v܊*:MWPGotu# V9FúMZk}Ԋk%̪3՘fCa J5otn: Sk\MD,5{$RjG(űr@" ;nu8=Q&t+>!а:f"I asN &%ڦeR[5N9+ j{n$0f19g(MX!0׌zǍ?8L _gs8eЋz \*;Bčnޥ<+tΐ&Bfb;i-^hb%%e@侔 Ng:'P=ޓؙ'=mqB.]?Xt^k(n[ŇV6fF 爽Db#-Q1&bɶC?ȴ+oXW%m3Nxj'vKī0l˽\xcHו쁉]$&0U߀ =To{1Z7bvRI,w oULb=*qɀ$zo#Ǣ{SDZFS~_1!u"՜ӮwUP[ԊtxRlE2V F\Rkdہ;  U.?e]cUk93flf[oꊊf8 s!161G"kw|EyUba +P뒰ruUc]zm\`n#MH[1HШfYhQ GSrҌ]d5&P.a/YO I&gF. /՞ # `7k2AH [0hv+  o؈p` 2kck׉7쬝n(Րآ&(EP݊ H&Gg'i:;뿿~[DvZo{F/&h\g|t:f+c[!яl纆nGC,oedxYzm|:lMwr6}ztjU%toԪOmIVz:Th0U4" VH**Z/b^̦zA[D3G6*1~ݥ_Ũs[>1XqrQXq+{sUwza6M"'NH@&B5/)Nc $ĝQ!3 {{3k1K| b/$ fz1^ax< L{Z(iCkܡY,YFL6eaƺd?9[[o4jVq^p^KTE#i޵1 3ەJ AM@m \_}f"b f<ΣntmژFb}l7@QEj4F DiV"]}r*(- N;c蕽p]J^iDM/U]xԤ-N0 Z4%3d6BU,QRx9 '3oM"d{a.ГD@teZJ= g D@vV m +įw9o )b8 4$@#+Bt%4f'Q+zRMmʀ~NZgQbprMӊA{wghˡ2ɺ/zvoo:_$fhTzBG:l3[ƤyNyCU-:xƊh8g!Lᣊ]$af]>7 .Jw_Q S:cĐOe"/8|xߝF^wo*IA3dV[RR~qTJ.$pM㼧Zgč1SXXb1Z0PYn֟lM]zxY`VАT[ۛ7}d"@Ed.,hJ4w `p^mu@YhxmFo-#rQ|"/&ULuYc/׾mo?eKvRh؆L\7|! sdZF:U_1r$U%ɜU0\J ){PN :$52fktBË(? ؍O?z3N5/Y0\ RN Ⴠ j,l߇u7{t7+N 7<2:YF KЙFíA c%XLVӎr8Yy{ժ8ܧ׸XmoBmV6v@oX g?fLE(1,bLe ai^F^"yHC(Emv4VJݣX$ΞrrcdQ  fSM=2H7ɧ83}v^P>Gk/=eKXnYh/pGK|DTbT30LW2z'ҥLbw,E6K!Iq8^?xGD)7Rd ]Soz5Tws}CWR"K Hsƛ&g "E?T (tZP:*=$,qģ.1ZHŘrεk,R+Y0 k? ݞ)el@RLH kL,iNdjcʁB|͐vh֗ 4 pY;N\^^WW5ͳ~{뿫-nTO(ƓZq޳J^t"T 1'?QE0d}3 %mynlְvzݢ_zר{/R9xciaNV}ޘV-鴓Byyh,幻ǷlDAs \ T@'9\9vH 3=aqi8vJfx3BEp˂O ^kh:Nwn1ݺc[wq؛$bŷ]5ݠV^EDj2mdejnSq[)W IDATe^[nfx  x Z(93 K> 3'|#_Ȗiw{&74qB #Kb4n1Ē;U`8^&rK F;nQ=mXxT<;Zobo%N8ZW9OmXԺxw-EC)HPҊV\ eꔮRP[3BS?BYwoUО/[OXmT٭{KlǸZR[mQujT(մ4w={}NԽ@]_3Jn#%d-vOʄTmR?JaF%>ߔ3BAE]T1,4C!ja6@i PU=nl9,(Āqz(#0gv12eTdfe"U_-Ŧ{=B"~t,l%.A'%Nܾ8~, {'Sfݭ/ތߟE.F!JXT{$q=;p/ ?bԨ&K kua>p[]LXGdBoYD`9/լV rCB,)n NT5i%[#ҷZH _ٺhosxkE\ԣ.u>VēOHv#TuDpM+<eGVh؋樽#}0ي+nLaZ0YeǑt=QcK$(zz\%1 x3Ϙ0eeDbT53Y2ZHd4Ә'BdD.xjhY2 ~䆇:~ï?o;>}b4??_} Z:z7s1\ok>!cOwh88EտͰ$j H4~|=9_"sL(ĸIJ"uH_{nĘffʬmvrVq\qC)*+apNZF^`m ͽ.`^^[9lfĽ-p$|#WRk]4V͹-ymM!F:|&l:6 G ꠭V.N*z'Sy7Da\2JA 1bWOiu)1Z,l(PHa?OMT2͒R 'q;B>yTf巸1;=??y/1ƈ__4O-}AI]ww:ҫK㉌$iM"inwi]&*!@ @bjkSWu"ǂxώNY_"6G-ې5fv>Lp1`-c k)%ׇi__w?pTo Xhs-.0̠$~0c &\p`NLd<\pZfI2f׺ _ lmeM;Nq9EZ1\P#QiVEHEMXmq(B6RH)ZcWm*f'\tjkvN jj}w['q G_E>} pFܽ~)+o;ofd[Y$#QZ +躧!dׅm"MKһwj\^vp39gDH]M"0Tq!oQIKQLc`u%_> =SsΟZRJO/.ݴW_G0P3ru} =pc.{f@#( 3'aϸ!%)@`:օ !{+tG^T!aB`ARb/I(KE 6&Al"[ Pvv)mj4Ȭj'[/X"B7;n >J6oiARnF9n]RŒ 㢾X:=ױTYE2[VR ZTaβ,G 6'(-;h)I"`n@'0:/ik S2Isf ]Mk[Fn7&nWԐ,[߻Xmwn7)YmE k:)g\V !3e!KNHr꭪ݚ"Uk@WbY=))=nz~!V0pqdNK'?7~?w?G+;lve'DBlb{b4 *5I8۷.fbBk>BX"6@MbvomyDu 6nkRhƆv+*sqS pBgꪊ]jco Y [=xaX{Wmpgӝnwbzu>u}m6{IsP>XrǶtLJ8:s>ZgU f^S `7h܌3g9nm 'cو5 (pR{ jv^x>ڒ,/v~v{tvW~p8|i1ƸbAqqqǏc}nO(m~㟇=N-$Nӣ L^c* 5XL-))K;x,0 Nz(SekHYf{k]; e5ΛPeTۏwSitk5-VHES"vu ]#dS:R1C0hQGEfp]2C^hXR)I]AjfC\/̖aʩt^OȵUl@ÅCNUzt:&NJ3YfTg4³:s`u,m4֝F!8!V#4kE.T͕׼H1F+c;-.cf(!\ 6lĻG>GW}D^J!ՇOq8=Cabaq83rX.v p~o_[Һi@܀I`LSqT9a*n6)FF3Tb:q$X[ kn zvs:xM&fь `n)[MSD2⫻?}ohO^ "^yv2;њkmqt0GV&N$ H7vQnZ+tz[!ό\XXti *sPl}CX1Y%5=U8tёy>.j,Xӝf(֍ݕ"1x9 A?h ƛHJm} x*dp|_tB8"ޝWoMCv(?'|M(  ɦW0i6]!;x47㕩^j{;9 vf!rQa.\M\ "mڼpkf7iBgYv.ΡC+ \5@X4f.jȪ9#*9;1B\Hc[Vܥb ##ST,&TKzb,z|P3Ph2mG-V'7S*%a{8[xh7sU_3J Ĥ5TFksb' ޛ= 36:3u@"U ` Ql#]j>>pK78wIc-ISJ=? N1^ YB{˜򛻯[Ǔ_3*2\a><߼eP<$wx#R@D)=qqq H!C+ݸ$ɣ/_n~髿惶\%-DBb=oZŪPYԫ!i2(` tFkPiU1b'(,BfڂĊ^waX:hI}|c+nb=ob:"F*ΨkGbUFWtbhCLWv874.ݸ'oF9hMָ ux;U\;gYQv_bno9'~ #|bϵH[,D: n=qHlijG3`D9P._^ 8]Lq*f3/[l;%y(wD!DCArXv{7޾;ijvWjz_/~滧7s?x~AR2 FP}rk,): {+"(fsFtP Ա:omY42Ɖgq}g躢mݸfp L; OceFՋg5ln ZlHrw#WqⲀ^ۚ2 \MXKg!KAEݤsm+3٥;uǶQs;`=tK^>n=h+FPr/X{ ;#=~G rj7 ]Qt##쩲9OjdjSy\åjB,(L `2 ||(H܏{OP T`f{?hna1#yB~ ݄W#,ӿn}_M/+_HFp KѺ?m'{\2P=@/n/võ_HWj]V |cb4}OCD U{Z-`i&hSY T)ԵS5֝\c)d* ܝAe-#`a91,"g,~`J^\, /_B^ ⮈`%Nz5De%73gWvbhʏ0PƦ+]v!!x_j"laUUFr,{tH5B3b? =^r.%SHR5-w9+jq+mӒijVŌ6`^{ vOnvl>R>1^#K%|q&z3>Hh/^ Kc1,:}uj}%wf\֚s Ƙk͵2/ݔO!RwfD䊈ߥz5)0yO#ˮY2sH2L@LB) H8MEVBvĜ"x$*Wnڞo-jxm\jR,)#b1 Mx<}>.0~D~7s A6$MtL>= GHrޖYI `{:A P-AZ~yO?@l4D5I3ϰInDϰd8 al!1@bbJx "@ mEw8v$xt8pABkvжPM"v΁N> UҼ]*j,oo.`އrɮÚWrU_C}6k|ů c- tv)V3^TTxK,nr\00Fg eCH w 2[D= Y7`QluQ'j(ѢJ2{ CLÅ w?16xI9MMB#?ۋ~xBDO1nSI*cpVzc i}#9әy1Ĉ4})G_n۾?.! @6:wKzMUg8E!ꅜewo*[ÚM auK~sd)GV*CM]{vhW:xj>EU1"ǀ64;Bh1tE50dHѣ-&e/1"8h~8dƬs}KZPD!9`}(E\$ZYDDK, ʓnMjs_-$qs<)ҫEWBErXPy,kIU#XZe2]h`{"(u@.=QBE@:SV4=e)&3\O[f\ڔDfN'\ [  1F 0y_ʇR{?~z?ۨFZI(ƪ;̖H![هUG1p0Ƙ_e1Měm C 0B D DfAzŔ4ŋ6- `N@v}oqDF{)11"F""Nmj"BV4M~dE!K !bsuSS16RdvgM0bR2 s}N>'R]<4]0U.TQ.gf]}!*8I;j *өU:͍DLٍ~'蚩m7̕YjHyM"v"V IDAT(sÀ] k _bp-5$' Fŋ&|P;>~vާp" *OQ58kSM* m]=6Y236 G_^^QlbfyEٿ?y=GŜ,H; H9{,G;a0;]!99,^꤇i]iyE`Me`xkg2,INHf4K)H?A5"""q:ju 2ϠR w>b@/}o緟]^<b4nA's`x{:ŧgVvcmR5ſ2 LN @H rV@IS)-ѱmM{3+Rbs8n'd3Chь8% Mk_G3ct1hk*hl {J'G&U!)!tO)FDъɡߠI#6Ap]H"Cvt8i"ar(D򎂆EBWaI+yDh@ 4E^ԝLUHo%; 0ZgA{a7D"@g-H@c+ȬtH%DJpMPeܜ3fz<əvz}B6<95ifw8oS?["Zpp]h>%?x!޸$*9HO[dUVQ5UA5p,] E&u b؄3;UntuSp2DNX6c+62V@Hz,/b;0=bգcx3CBK7tIo ҏ64q*P+Da@l־ c86 ~SB#7_]W]֟~ 8hr+UQ퉨8CvT5ӌ("SvFϐ(u 'CYL%23By;YNm5!# ;dRck38T b1c"bHFbhfU-Rm7 "%@btrA+Mf}o;[V QiaZvga`lrY6i.X`1%g^%xC]1DdO-yJ*~%ɡ'U[{j&Jgވ8Nl$Us&p6FU)D;֒2kc!oD=IU*ǯ:Ƞ >1Ɂqx!kBRRlAoQ ɿ7'M43Q+f7|Q4&9#]XԶ !`٠i|򣧐 4,mtuu]; %/ׄi/-w3 113c6eb (tk&Ff{,IhҚ&I*Vkl kG/HBd䔎?!Lm )gW;KLy1C#FAM0DbF  9(ڶUt! Ե6we S"H*1؁]0&`jK#-"IB)*kWm '՝.=;)`1g^g Z-,x"P$cpO=ytb_6e3Ufߕ$TEl:{ 4ߝJ5 *jT: `~RE5(yDLNFzD;%nvVGփJv#堐ݛꎠ`= sV-5M!q3֮Bxbۡm[<www=~ۜs?8rįQ+ߓeEK-JNw=x|T Ԭ)k-w^i,INy-3a;{LĔ8zDۭ-}n|y 71ZQ>îMX1LM3 l. 1-/p?Gvqlv\P= 89G ׊=X;&"xDpآS{aRVT@uA*Ienz,yYߌlo\9`3~ٻf;/$Q`ꈨB 4@BR&YYPlEe hDZR \W39+93)~qfzEd6r&=bU!G]&=?B Ut g Q 2Հ)H?!vZenU4e, 2"8.=x!!t] iG3?pwGWo_{7>@ #{1q° EؾV; |1SE"#"V\ Jq*S,ad@D=-$MD#Wpnz]^][`D]g]];Ϳ6 wPDঁNRx{ !¶mMtG2nJ3鳰 SM>0ȭP]U>|#SgMɼZq-}eQJW]Tk ɊVGp߿r"PYS15=\}wJ]i -/4l*}h=/'(EVQ=BΫRxW[?2@vU2=؜GR5G&"sf |6JyĭmA @K`3o`,s١:)H_hA7S{ˡش9gN)}xq*XfFuZ:N9gO=NF wo{p,Hb{B;m"C("B2C-Sx]!()Ab]bs@BdT"m3 YcB3"hMӠ{41b݂u5[t-P]n=xDhp"!^_Ms v FhHo<#BCgϐN_c8!uH0G uW)&ه6i1|XM9''EY*Z&g8N!}(U/j\K$@c (cQ*TEhA p!T1ܨ.SC M l4="WHĴH <'^/)MA1Ĝ`YKŃa^gf;'9=uofn-NÀDQp׽5w??Mc_>=1^ƞ]+j0aYS#ځN'I;V.$NZ|\:`ŦmƀYHKǎ4@ت |"T{R+/[buF4{{C}oFmr͐^?ltBwtQQrFNq Q{$_LC1WwYNW{PⰃMrD!$@r.ԺZ\jO~)sgWfu&XmJ!5uO2ȃkjȊXry<WXf<o-_,~okcg*BVDU{ޯBU,Mؿ1V?"=+Y5j46~ߣjK@]/0?M؀@v_/jN6ZaYCJ.epB07RNDBMt<-bӠWl6L{->0^_9*3CW6o%V5Ru93NC7+e>k>Yi}^ [$إ>vZXܭ!=,"*cWAwhsK06MpW UOꌽVxVU4YoL>pgýY7(J+(BGeBi(0%7p; *#6=} l%5 Yp<ͩa~~~'x<"ƈׯ_g"^=~;T^}AZ )vD<0n!.>AtSBqnS/;0F|Ʊ:Gv6M~o Etݼ88}C@49;`trFܝm >3B!:C8#" { !Bh;P0N4΋{/^x)/E}>hP #2(8N@Jn,;iY|7..E9rT/ 7jrv8,+V-yHԢmMƦ:QK!7ƭ ;'j^6]= eS.FEIQPݏ&9^zUϓKE#I#e{- ULbA6LE/p8\N74jq5=z-+D|#\}f|/*ñ/t  P@țW|cFdLbi#^ e] [fET2]X۶K8Gy+]?Gs5M<;]cE)ko:qѧ f1rwiqqueb\wsDڅuӬ}bMmq&Ov4e7-q8԰' 4qN!/m4&UE# t2:!]4 1g(4gƈ(EdBbΡ<]^;sqLa|Z=%ӍVC:,ڭthՕW=W\ [P«g9rόzB:uLU-JiU$%k@_ātѐ}Ӈ(;yu=qj h|?l;Zq'2ha3Ļ$] ?@oе-Dԣ-ۦ雦PU}oo~k0޽%y2:s١eӬu[@SgB1lwgl8Dn6 2.l&ѻ?[4}o'_cl@LS)0[a:Nb`i[ s7}o:c6(Ljng"6=nwhSL %%4f`L_ẙ2g#u݆\f!{R܋X>h.>U*{uyW}Vߙ4U`jEʩ.*}1?+7,}P둤q9}Vgt2׸IJ`(IP=grdEJ@M|aEpNP j1$J aEuhmסmlLPд-G.lWΛrۘVmKm(Ō+`=ѻ)ǻBOgfD#qOOt:o~)wwa,z#%4%wvgDDzJ Tl@PfvMPEP8u9yaU0S-]ٗKSN\ mD"t' sv:jc"@K:=*ͳODa^ EfYt$NT6"@]IݳmܒECIMQNXrG kbw.#gsPAQd8&}~#e k 9nۯqL]^=yd=Ժz8K|vRuC Elx9&<H|0P1!vk+n^9<3So o"I&XP_dERĪ-qO(o OmM!n3qJPlaW 6Dl:# bRs[b/E w~}ʯ'O󯜓bbBeѱ*;r<.6h>'q1=~dmcBgC Owr-l0V۶F{7: V$٘f ?r ťz'oĠ8ՄYc"+- idq)%)J^&".Wx9n^pi!Z2x~C`й'"$q]%1:;O 5^1 -E،Fuv[(\ZfZ ϣ!ZfD $ ?6N=q?9W  ifllV0_qbj6 L9#[IU5LD;!IPB\H0 ȏ?hMu:W-??l y!dUM5 ../\=5D &jaFC#"3v1AMk3~b49NEHvK!5 S^;]=FEjF_2!qg/aZ788-Y$zB +N pCQ8NtVɻ23+@y))TvWrNu0;SDkC->}\ V4tTdU|L:S<]La/3%Zլ@Q&0l}b+2A%j b@kЫ" Q{`(6n },Ƹ =IS6ci5mv,sR*di@'[Hv]Ǐx<c-S ~k__T'! D4O86O; bInz;9ܼBյ0]]|w6M4q $5,@I82PZ ?hJ[< G[˂Y]&sV5zoi&o;O=RJ Zuc Jgos zEaPut+.~ V@m["xF/}O6B9n&J5O͆YM5ӈDM.I%c ///ib_/~*HM&$+v;|_nCwPUܼi68"-#^AJT`igI,WWfcOJג,}Vj^tHE:DDgtIu)dž0mS[{1M1`{qĀGo;=-BTFn;t 4`p] ًp(Z/P-pvpf0~"6;&ZC.W:Gb:J=Xt锟g#,f?1I+$a=|Y@x6ђG/5DZw3Cf:0OJ iT}o`Qɢ(b*^C6ho)  ,| l# =V6Q4zxSuL@c0B'U,/v<-N\W=o* f!5EO[CςwBeΤKoZU.nmx\gy 9b1{*Nd'UV B `<})"H:*gW@VG:<^/>v8W$EVJ"QBKb=(齜7lS۶W6Ԫh;E"ilwh!pkGB`;Z®[ŊN R0:֣^1ª~ND>JS3Bˢ߲9/RNE64 B.vg81k#P1o@B`ºs9yP\Qw{Tͼ^I19kLQ1!ѺtB>kZT0B%~I%@lu!:[w;nU꭬Yد-KUaUE [e>@jHS+7HGU1ET;/N;,o\p]q֠:e.]1XԹ<\ 3--i!"Sruukq8q8txzf||[_Bvɚn0././k|?gp/! m|@x:͚J'T0 C*xMK\__cF̯p_Wc|k_'+@y!DSM$%O>s^ Ըlcl<)P8M`(Uq(9C1џ9#*G(aM`ExqQ0Ãx_y#޿'zoso߾oKp{ À~zqKh!wޕ6;0m[G7| | pRYi&qj'0kI?7BU{9p̖ e4 `MFle-e ΃=!pO tV \sI݅(Ŝ#+#ʌf[[-^Y犔|^Zs-`ܹ"I[f)1csb3|rɎ1\f$cJ>`pߑyvN E NQd@ {Ч%Ɓ8CpૄAeJF;ީM ?`K@QvDȹ@ޅҧ h ;/;*s]r@x!^жɸR4QFq@AKMH23R8Sj+Y^P~Cwkf.ۮ~.C7%3#vfL MpMI7YQ\J)ڼev6.ƒ+"eȤAUMJ uUVՕ"Pd%*U&$~P-u`f*n]^fGRPq\>ϔ#Ίxa: "?T7H$!Gjg Pd '0H`%vMzv5_/u1 #޿iE8|;YL̈1an7vUd]yN=Vm^7Um uP⋹!Yɭr_$;N+aD3?2zHlmDx nb4>`f(F+j$$Ѥ&%6Crl+aYi JV>o4Zci f/zlkNK)%$Z ,+v&xSd˘}amzMuN1% Hm,Vc YYc'MXE p$4Ȁ,ĬE [4'wbCMƆM!L9[8u9s=y$T~V,%'g}*kHn p[(S:9!Z>}-Ԉ wZzRELfXϓ]=_\gՕJ+%٦Ҿ؁4;LPaDD'DAHIZ+U!i&a/{JfKd_ҟ/_am@MorV_U&DϠK>.zGs #q53~_W jMk~<^ iqb{㈇Ϗ)͛H)t:[jip!|G|qo߾_埣mZ[*B4.q,;cY Uk\Lr(d)mqFtߤ=RRH)l}ISz͑KnEF~wG{_)&xkB{kFA63f!aj&1)!0g1*5\ccl[LMmR~Ar)ܥh5;osV`j8+{,0Lg,U/Ѿ-w#3|b`#dRfg lJ`C=fNx9'aTZ0sZsDx^.","3Or1Q] Bώg G"pGGK$o!qdG8ٵ} Hp6@ˡ]i* jJig=s+;~Z`ϥmJ.`eVWOup̊.WV!PgVd}.` H99kEE"m̎gR˹Rl/ٞǍg2r7)K Y4hFSM}ys $J_ɮW Dɾ *>|hI.ENfֻhHJJ{L6T>hǔg)IjnwMR2kd9XDXN@@)KX Q@lltt cPԦgk| ݶڐK@t[Ra*p0:^ⲉ}^0 lRVw9rH6"t}mtzsJ`<\?O<`g?~xwnm/*?8H)_aەRVKy%L PD_\lV\;?ΤTRR~ϔaI{е ήm̯p-oIs Cǟ~R,z"Y߸fhbD`F;MCȠalL@}BRCj-׊jJı*+9w$lU`VDʲQfJp#8>T}!$voaٿ"س#Кo2ߨ./unRwN`]SѭVZ*n^&[É/Xk4V᫣͆a(C4ac<4F+vY f /ŋeGkb7VT^REL*p+pUC\JW_GTF4$Vd/e}ꅝށ-jq%Y/ĆX|6z6K[)+u@vR(^]|Jx[΂ H)mƉys$h뉜@" n&p"xTg 77C\bL^6p^y;$Sɺe3W&*Q*(S$MB7B !ƨa,ӈ=0ex͛׋Z5D Am-$iD-ף]9UPŸ"(aM؊jLdO˞2@k͆FlGÂ)=Tjzlm1? GM2d/Q4m[L@)~Lj.%4+, 9a0> +U8F9Ru2(N:E_J(z,e20+9Fdu9@r/kc-*@Ĺ@Ȑ-jE,p^q>YMCCn@sI&pBL@1D7M6[^'DpN~ ΰ/O~7N o{(Wj3U"2yPj0~p|y<8ۯj%F$ >xi0ڦA~R>gAt@ʆrFp>CvL1ET`}e1'oYA0AYGK0g#*7cEujKb\/9(q%o`/CTE8Ċ\3H ka#Dsov^}Yy>A9 <]d>MB ߩDmՖ*3h@AI( }ɐ~Ö}!]1v{4+ EsaW8eidjB-u6ńq1sӔF.e٢Ɖq>фe4:寜yOg<>>; rR^d1AvYզO6|@ui¬leGḦq.wJIiT"BZ Ei*u}eukfت*#2nu^ZWYUp[9 cQՃ]?V7=Xuu:ˎc렫FTBg A׬Pwr.qzF={ A*4*}\XR_3CskENUifq5 VACd:tK#j"H%UFO$F"ڙПPLB Q} 0=@\RiD<.Y J'ZodH{Dxu~:[l}/Z!R6AX^2 Y)6#4miÏwx;@۶xqwrG@K$N3G$fKaNfY,8Zc݇Xm(;"B 1% 3N>4J C==j EM`R_leyHg<#αHmRKle2`{=4tA|zsCί6ٖ5i3[ωM)DcDF$ۆ+b 9X( ,0Z9+gDKIܓx!"dFdjCU)B rTqOss?)dB6RIb`]0GĢȠH#!ٿ@j3!jYpt 0Q?CGC'+o@ P˺| A$p#;AցGB A셰~cxx(jѻtW~՝pٍ:nfIw`rV3iWUT7!xZ(?G1n7h<<.ђxѶI:<~^ X#uhF'~7oV3O&uᷠ0sq}i $|p{{(KB)Ija?bcvB8#>~(X=vGs8!x;r H)-@մn[] =< IDATMȲ*+"F-CC XE]-JE8#` f#3>&5etUhM%Q*v{$}aP{i ^FXT򕈼jC4%a$QK(ιbKV6+dh"o\:҆,Z͠ȁ$2 K4&wGl,j31n&zh 텰 :D N $v GXٶ^x_2_wwp` 楖`#A&lTTk߯Pj.n[dΡ8 0O3I{>^] J<\as`a, pGefe7oy 0 %FJ`7smT^JPYFKcQ.jpW_)5yu?1 #N"Ph]Ϣ=Dl?BP@sxW>o^˗hfoM4o77"L<DZ4cD׵^}C> 4Й H FfSB?J ,PUPj6(6srš!6cg}Wld\" hc[)bb8EvKyj[zU :"Ķ3h;z%u-BdjU:+$&􏱹(Ysuk\ՆǮg vP7ItHG0h.䲔44V#7C_p-CP U~f?6+ڟqŢ·[kW4mi*?Ԩe*9MDrDE9xg(D:( >.-:e8i65ڟkvJd'=4‡.~ Zϗl츳xq9#Glsgs _ǧ%i$ڟJq~ףmhV81~ߗtٛ7+fW]9pssݾ:Ϥ4 rײ`hJ@=#nnW1YE1Fy#`+"B[8~)Fӄa!4U5C ºv˳"X^::ѵ-1Msɢ/ztUh"!He1UD^x)kiེ 'L aRRB?υ*i*,RR( :(^%Y,j2\dΩ)ێE0s'5Y 6uƔJoCuبsTe!auUg&/jɨ L N$(="`=0N$`j@91{]@"kq[p"0_fr5Xnu$z _9j8a.eL.j+\J`赽Z-rO3pkJUtc>"rDo{[(5k A@ x22^Y4*PLm.eAN%;nJ/>Oq^#׀J/{19bAD@#ʢ5]#S9A {)Lz̬Fr^Smo.빥Mob$=ڑ6vC喲0eyV,Z`('+e&=j~#9O蟷{2U 2 e6ӊL|-]{Lzl@L$ltj1@fW+\1Y F14St 4 6G鋨Oz<ٯf/Br nVTq.V×B(WbaO 4m~ <"ٲ"(KI/%޽{ů~-R^Kj)YF3N|p^Ln~:W|_ҝl 0ƣbLE 7׿Ƌ =s:mZܿn÷~[(0a`_y4Mmطi|ݯ7_/tIY9^|_8VhEeJWAjIe 컠o5jJb2iW8NI3)T.%t)Aˬ?aĄ&%xNZD@Mt˓jۉ {Ba@YQE_+jU$-q@Uq]:gRȌ7љfo3v)\; )af69^Y^;侓 FEǺcXDN#@v"G'ȩgyD0E1dbd@ A0O!Ƙ82;:X;`J@T;!8ðSx⎖Ou3 H|8HϓW K/_qbkUXr˝Y{m6]kѶلOއ%&WxRh4Qɔ]V6lf){rmZ-#yvhVŃ9,**۵";<o* .EfXeK20Uj리"Y)~JC]e GLQBb. I>nNyWm6+?XQU:MR׭+%[ꃽ͌ k&-B\휼u*!=:Z2UoHSd Gi,5  {˸xsW_ |paW63 |l'WF l}Tkv;٣T>î-B'#D]!4.Hh}2\VHS5ft }(䊶m8n= <|~~g%bb8)Om4z ^,%k"`PA\fɶxU? ^[_Ƴʑwg8~հynE,٫w)4QK֤L#$G*uq Z鴘9AͽA|.U%sVUd rB9rXt҈YEWfr'– zEUeܱvmby(D$"r`'"h1* 64$? `u6 Tx#-.e~b!MK@k+ml˯: e/Pc$ 3ay# g8Je[* |9o8MNgիg fSeWʖY@1 CN]hwww%s!"¦÷"9fBJn+dp,>x](l-&/|묶|`f,Y/w%,\oE۬|k3*SjaA%"#aӟk.ׯ-lAʐ|- |M3š2mR* # 8 | _Awe)9+n+ge\ s3?fzOf`'A -U"Nevș?HĬ˾LHj*,`_с  gݶ`K@wwpd&TV\*A(αO $y2/y۶"g 4T%.[Zw8nе-`JM ¨ t][ #_~.\ڋ\gU*+*SM-ߨ T xxx%I癌kvcjDB(K>A9aF;p"hRTe>EtIP~-A,xfhm'05y5-P#Bg})a2k/Ef^93_AC٦B] kGpKts8~ˑ-ۙX>hP ) Ȼ'uFXprԭ 7Äzt}y#E[/#f}[qt]O5 >Dׇke/4ֻ5A!,p2rtqTelނbN)s}|1."#ńi0Z/)my- B̌5j®⚒VQ-@M{zҳVϒ@OL=~μWt ۟-rBuyE+٥xqp{fx~z~"j۶(j7ѺcA Av8ޠ{v;v;-)j#ZS{(*k 1,#ŨY{/~[sūR\#\QWR!%VͭEV-5;84, BZ~q1\{"$ ށsA7yu 'gR"Ew,-+&{8}k̬-'~2!iyE]"3%x-bGLrBcDxb^0 p XH@"Xpfhc.sf,X&0BFZA1~q޿i5PJ"jB@۵`/S Zlg{ jt鄦 h8pt\lFH޲:-IAmSGCAe;0{fOaG{tm^[@׷:gd,%G*&kFAs\jP4UY5uݺL'ກʶws?t= Im)QSvӪgҰeAVꯕGg`hvpi!$9fBHuf0c7Zdnߏ Yf\l&͎:'HhVn;{Ns`wjG:bk/YUTgdMG!ac|/#V&9֠ ,X&'2`Aiu$N70Ɂiz][-Hi}J}+g3iǏm8w' 8l1uĩ b:hHjJIT]ס BS9N8ߙ&8urugt:ab\4C}mez) #>~FGLaP_@@Ʃx(,d=v_np83D}j A׷H>d͚2j($\E-9xP(sRV2mŠ~Ź6UsɈ2zG|v^gq\btA *)W+B:6G8!m-"13iPaضC IU\>aL^CL"\hl/tC,<| Hu_o 9`HՇv\QO,l Q2#YRmu 0t~˾VfOyMB6M憹cǔ8|Shvfuu((&;p_-Qd -VFm:jjYeARgczHrL!>RBJ͌fJja=w{-T(yH>Zkѱ =3,P4gKQ[ =R)I1{$ Mc ޖQCdLkqd)sV f-eIS<#J'@/_~_AKqw8WUMuY9g&xtՔo;*1A׶0 ޹f8{gK->=>gwww` Ӕ! ]Μ=8xq.&i]a#Re*>Nyi]YKuӀ?o%2Q\Zǧ2bd=K?LQ<6Aޚl)V fʪBƘTޜhBpCimzY9[ &B 8!}9&eAKsXWk.%tILj}b|dj'{QHy:f,1q,82c͑eecҒazVT@q.o 2D!4pMPɛ1!% HtADdeSN]WίeLтN,C=)@I&"jN^2!|x*UdYڼxqΧ3J:#6hV4Aq:,%3TL~_L+!^ /TrQ 2U%cURYo̊&&oB˨sޡ{4Ư DDKr 3Bל3XA z˰bvA #]sHNn7Q3W d1(,Tl(rG*%A^SeSAQ8[0lK,Ż`ra/f"{HT=y`RdNXN3 V%YNc|"IY"})2qJ!()% ?7_kfϛjiOHQK| MaөZ u+i|~;Sͅ˹ È>Z7 +)3ZË/@Dx'<=WD16+k 5v H}|:CDk݁Cst#a(Vyi/_A>~O8|!߇lpvt# eC" #<~c/8y9DO9\ 6gS/tHA`)%@[ܺf\fT7yY3|XaYΜ3ĕ̬8q @cQe^|OlsJ~|˖lcǣq2R$K˂@Caey#E1ry,V~<hP&Y483"{||BJ8ZMPv,g&,bHK/If&xQ\qкƋT}3'Y ^W÷Z #uCX"x^?=Į:j~UFإ39g6/5r_R. UYMV8 H;ڂP,±R-ˢKMS-s/48Ȕz`D|`H/!E9kd/ *pEOG z8pDsJ!Ns&gVN634}:`Ԋ,]Cީj(t:[wBP98Y90g3)%UBߒKr9?XwE"{;;7ߣ{ÏSA/_@sP>~G GnOBUe&?,&@ts3T;hͺ"3F }TZKD)Ue!xbU:N{D18Qsc4iz;Y"4hRB#:dPU`!@byWJmbe!ˌRؔ܏. bAEŤdp\ptA\˰T`'Ť:7[,I~5sK.WuW/ 0 /QgSWP(#$FQ̔fǎGHoɮ\"<<h6bQkFXvG0oÀoaڶ⦏{Àj6S!F+F,5?E`_}9^x FK^H aW_}"w߽AuyO:{s^՗bF#xGp0 Zq'Jc{~ L'ebF~C~Rq!3 sQy?PvamEJ;EOh#R]7M/I(S@H}H!o 12އ%g3e&{S$a⤾3Dbd}.*QGbӱ BD2_ \ʼR;c*HA=;Bq)Ÿ"i^&5` + p70a1W|O@Nubc_1v h@KtJc5#`?2K'wGd_Q;F CAoql=a>+(wBq*nxj]&̏;UVȂ8F}?_ad脛v)rV.m=u98B00c%,0ʡ45:$O:nJMf6O' Sg~=>H`t/"33"ĺK9~Sb|<b0&Q ?-jx:L{2$na=;g{/2 N!ZxMѸ& &`jM`SRP~K;ĉYb#*(NwLyW&[sFJVQ\JUld:0:bt&^sGN0u<0p1R3@fro/E<[kig g o*q}-)hjGG O}?rFKyDF͋,eN,Y+*AvpaTOLW'qꢎs@a~0X5.η؞mZ/Qn:H}>VU;w߽+z󼿹8z\^clwwl8;`\O?} 1OQKt8^<.\,xvu,Pիo(%5٣J5z~挎+%CUEpx>m694p^e9wxlaGC^[ep(f쿄 1ω "9O,H!|cbZh!5a#B@, B²USHo8:~y;.g%Va*Zdu H8atԘPzKs0L--: gsLQIbSs4@#W1@pvcq+H 0{7Fu^{Цbm";eG``#霏|S7_Q/F0T}`]dŋb!4`zA:ft֋ͦ!xTLQ@G֞Rӥ: [ˡ{!>Ι܋+B46|pcP[@>V}Y>T0V @r$pIGU~ɁyABXDn!O+IF= #^=@a@&cfd|RU9h"b*!7S7v'ן]T轘gQ!u: L}6cJ;–bx#/w|YLQ_ǒl:1=ffvo6]<E0F:$"Bb =^͔{8Ei樌AL}<K{,0,0뉲,L/A#I:b\PH[7)iqFD-9 e6=f h 8:"2}tz^}<}r鮳ȂP(̐tꗅ(Ex;A y=8 iҸUa*!DBkMaж= Z|!ܡFv{s1aqYF|A!`v.o޼E,p~~c >#E ED}!ƚ <*~{wi]J[N6y޿1xM2z"zG/_p8!k5"<<0AR\7k Oww;"" uΉrYxsSq/rDUUa$EKNd'bR a-V?eYDPUʳW-i׭\]7*喃us&b˻3NXq1˥eG6{Q;Eeo>ha.*X2cnEaY,I}Z)VKYi@LATh"6-IF\)}XuC6&pZs pvf6Mo% M̌޾Ɵ/=`^b\93, |]ံm?Gۡ*|WX-K8{ph,*zQ+,epb!'>gZ.* +roqs{"Ͳ)S7v{+A0bM`׿Avx ablUC<82e̶ѽVD\e X5<3onQW*r(^uęO6΅qzj@:Κ$e4#(σh $hz%1EY8Bg`Qc$=X.F )Xmz.T+ l 91#i~R!HB h#s,|[3"qđ5)UJ Cy$# 5! V;T[IkE)kB|cރp"#':[sn-G!0PGЮ!_k{ cYށb*1@hG 1ZO Ty=K\>tH?mYDXUU.yg!vg::/ЬY$k<6Şp8BhDdg(c:[èT:4UU2m!%(*eLJBe!Fa!w'Hgb3C"&MLϹDz~BK8Y눴 D:ʎҝY*616+K|vW:>3,<3oMfq&U]?Rq xW*!(e"07fFτ{QG:])0a0JsӭT"bV$^Ȼ?8)#a $ #GGPaGjxHtuG$MOLǓ-6 #:PA7!%"AL@@ ~Dl-5u ~/r&T=~2èʤ7$:7$#1C^(XOQ8_v[cqĻwq8%>%./8ߞY6 rQtG >g(*v VSiSe1SA)Fm;QVtKN `\‹rAq{{azՃ5t'uTQjVe=ω)TNIhmc2ݦ►T8%:[>7tNN;qrp2ƧrJLHDpUTcB0 5:HOX1z ~d8ZߋQ/Xt&3y[ٰMI`AXayc`J 2-3;pD#oel'"Hq$k>0(\:J .2`"R: `+ƍb)<+2m,H OԁM}q F<LLx0h݂p7,I _\.W3cc5Y p:+qq31L>9ќ`@;/-=xq>1Kf( 8x.η:y1bbď1/E^C0U-TՈ𦐽d\ѣLGH t`'4 NƇ!0J7 qJmע *OE-X VvD (q\h ~Y>u;)i{kP3;"-HO%2&edm-B0#E.;(WHiX-pkYeQ4HSyiO%ߘ)ϲS@DD7y4`O%4X1Ш^x0A_؆a|4rbza t4<#o!EGi , G*3Ⱦ.wmb!:FAfQ])D` ;Q2H5+8/(}(H??/qMQ2k45]XeTPL:vʪ2' q1jO>0G$>c{@ =b/aG|;z=Hj?wo0zhK|g8;[?²i4_9a[G^QA~'x" -s«U>>=՟&-f{vJ0x(,~8L $e]hg HǶQ.H=\ t] rY.&Sk!0.pl۬bT NqD԰˜ZԬd}g QI  Qf]FńEQ;+/uXt|䈃suA#V!d Qd`cLKUTZE_XAwGUp*eݩSl V4`# rg{ 9 񆶪{Mcd܌jUcW1y!(2ԲY5^ eZIMiVբ@OE'@Rp+6"m"^<(Hv"κMb s"λO@S:*mdP[Zb}Bua[ IDATQY<~zûr`lp{%^pvj̇(B:bLSm x0hQ4* r>ѩ0$ML`$\HDv]=[w)B{vIPi,{|O´Jhx.;=U%S]%ģ#%' `"Z1FgA N%RN)J>+~1 ,5G&T3}]} $Kmt)Ly&o@F)>]ƏCܤ(]G׌}$ԑ4])߆x00:|dAo"{途cLj0e"Gb>FƐ.Sr3g= *F:L!6 "@wCU(# tK `|V__ Me|4vGSz4$n|ӟRFas5t1'Ñg;#J4 ш~WӪ1n7wk4|v.1F%cGGzzgWX, n63T9^>]y4.9pE))F3, bjf>7h`0vY`Rnsd5NFu}!Y8t,r$S]UYJB@*8;]*Ɉt_CdP 8gZr9cm%n]UC9&>/0DHkQ4Yx("x~EFwl.qElD="(4cĚz$ؓwzk1&F,}\Y@^Ӹb,:9uP4p!L2NOQk-l4ŚyXTjPLk"G,ݑc 7824 80y~(,0P[ c( _warف)on92 CA[??l֛1&꩜^ q޳T0LcIT鄡 &T1H+l*3o_mׁOpv1XٛyV8o߽G,pqyvLtlVK1؞11pB D1 mL 8:E3mM{m WPaj1HKUrtT:"\>?GAg)׃Jy\ng(>)x؉${qIdܶm^\.z)F LFHrt6#'wFo)%ADQ $NBT5sʴZg͌ n61b$, bW"R&SMgxUBk;Wc|-g- 퐠>">M90cUK/~ ܺݻ3 vpg܍@B(1[B; \ta5،i4LÀM=uCE'Ǽ}[>fm6#&CHqܝ.&̉4vFCP73jyҢ| 24:phqs{Ѷ-1C]U8;[:ˋs&_޾ ۳ ?0x07328z{]ۣz坟aj,8 ΈS2Ŵ{cL w8?h+*E\bLjRqIb5I!i֮#!rQV"p KhEMzSi:%es99f̭irg!A9pċ,)4j060XѤ <#lʔD譕H|>2D1>£ci%UBQ`<"؀y 7V   cCJN(#7B-؋;mE`D 9X` O8s 3HUaR]~(H'O>cRJR3q)ԐAPuP`1`8EOD_a`g\%0x{c77^%|k`?bg}"uD#۳]ݩ$Y?Sg%ΞuGNf᫫KxVPL}9cA}HO).际ņJS|< Itob=dAHĦBȨtD!\b(2*FT1b$Bo :cZ`ԴʌmvVL Xvk> 8PwXv<6j9DUğPb- *QfYC$r+Xi0AFtShb4WOߡv P[6~c3CA+n/_Kch5@OOaӛrnIeVJ cǜ/$Tn֊m;-w;a/c#6ie<A#(kdooqwCϞ]bUZW 泫 LyQn0 "Gde3];O_dRMA>nGd12땠pBc4sj̔ѕNICF)!KwibuFD/ŰP 1FୄA|S;A(]c.jEdžr#ixlS-PʢT"&nq] RP:vAu AQxTQ;c 1å{ܕE"Co}YDVˆ10΢C`1&`>09bfaM^19'BɼQC`X,Μ[cMmͮx'*%Și=<`,Em g+Dt{]tpbZaZлqpύN_?E":+gEi٬V0 : $lJÎ3rrJ >LMz;g9Qcz`\23W:C}0ϝńq90|iO#'پFH۶YUzǶ!BuKY0g)4晑{s9x_-ǔ7R~QMu1iR_~Bgiʲ xJ&(m' kqzz"DcP6N%htd^׀k71B""8u%wqcH1@0iofp$prXEBDc%O|kX ߩ TEAb=qC1Àz|{e۶Mu3G#̕U1]8kt-u&[I5Uo-nnnnxuUajt/ȡW߾AuWz@㷯^plGj.GXk'a^?s|}?෿}áU iBys0zTˢܤD$$os"NZ:!c k%psYu"S*:Hzl8;tYjG !ֱms7z{wK5wÐfzVy4hVGL'RYʽ]IӦk}?5J94Q@>Ao8 O ~ W}|236.?NA@ UBt`Fg "A18GBw,p=u ]dA/>MkzpG9co;bgGWMnϸ u8VZ AЭ_s)q~@`\;9_Jy~(H'ϯ!4l h5SBccG櫺#Ȥ4*od$}c%bOb4è,$Y0_tH0~!w/>p@{p8smF sfIa5֫%qVVGmjpwb2ߩXG:ntbp?{-@X=@;绣Eo"^'Qi0߇ykxv$kOHs+r0ir*HAln(<ȡ ciY^?VBAx&Z]?zI"] eO,3onU:ZJ\j<igfFSiWfƥX )@aj1Sg0fzq%uq2JtJ.Տ)AƬi )"(/3̯ -b09.rrdg5- Eg QD:-ҿ DD2~:+;j:x<-%5 ̝:*w@QE&`zKggl$_%;R`tv (U_ejD+FsVdj9|B&L9Ym73}a_xlWX6 ~ӯp~E׶F?F τ8y؂! V%nqdVݣ, 4̌c ..-oF,3H6Lx̌ 68[/:#cjӟ|0z]?<ʼC}l0Ţy $Zy23ڡ+| ˥DCh àɵRȣe09Zq8CÃ>Gi|DVUP?*U!q7 19cmɑ|t5S"g6HzR0Iih{T@Mo g 3s?Q+> 1H36Ίe( a Q#05^ňzJz4f+,C@85:c`q v; 1^{ sX 0 h@Wf\.W 875 #] kP0,a n3TS쇂V Q VD`+D3}Q)GL1ExzP,V$Hn3:ڶ.%D/} CQիZ#JU"祴$&Z:exBJG|)+SqtUs '-T9 #81yO`;"yF"Vq( IDATG%'8Z0 S\3%GU `ŵPQ 3sYWٳOsCMi7TJv,锼5dԡ\a\d";tn&zT_KSS.hX ǸZIѧfzzñHXG{a`r簆eΩiWӶv\^m ]+{lg^G<F*dGk LakR |ϗux!b>61#8pж-+ պӑQ!cDp"E-Uf s4ܕD`O)('9I Ӿ@*ZG&9B~M>&SsIVN@Ʃ[tYdksXO `ƃJTLvo ̿c(A?@<&#kj%c;1K(7 @]3%$(:GZ9 d3F'nmݲdeقP qjp*b|\ʂU*ڒr+]'Emxl5>&,J 7MStWR^G`$D'Hp{{̂-yIDeAb ..0d%֫)/rÄt[Axiﶻ fxq_{c991>$N1+FC;]CTwb#ƗYBQ."ڶ\Q `E]U)Uzġ*'1/Q184Fܼ4 (XݔeEd~`4'?]TLq驃bzHfVG>u(g&~h)STJ\puř Gn16!}! !- wMXo*Yv pE|@]|._2E w>uܰD'8)K m5O~4Lo\=YZ,U۶{2^_( 7tr6*_a IO i/XHexMrRhy$%1g5sV/Y3I&&S2lUGb(s|u]al0#rp_QA:lS! ;[MQaE;ZOcN/fFu'AXH>!C:sVYyI*>͜\/iFvZ4NJ"Nes9y|ѥؑN& On/T7#BXi)o4RB4j0êih=E={970Y>#50|z$vJtF?i|p!) d4 /@ψ Ї}DM _~z).# 24yN|'2ӕ=!bc)tI#.}G!h܈ۻ;pqqme$#y"8[D,b_LfkcXX"  ,\tH#R|xʯQr\]^}=Hx~h3ND釘B^3ajg]gf? èBaB%{z(ԋ,p:p>)(O ҡfTt"a(Xvʻ<Pg^H"K1&"X>'aWMiSįbӢ n,Fv()=7)/u<} ̛E]vL@0r5lzpwP3׆[Uu/ =oH:$b]𗌡Nx2A,l c(R-o8@G9~CA=~i`k"*3G);{ۗ; cS'5Kt?X"$5]*}%65M|>AZm%(p! <]Cq;di6#˜ |QZRz *ڼ_3$f>$M], 0mٳ )4ZVF3M]id({Bh۾gI-:0_^lywќ.41Ƭ~4FY*%=8br-ʥkQ&]z *ϻ4*qwT: ]XK!἖Aqdd98baquX{~+8F rL0P%(~ t0:"n_Bԝxj/gC6L_rEc`q) m|ϿjYa3Y'B29<c7.F)UZ|W R}z:CߋM1u!^8 @< A"\z1n7 )+2t2YJJTdT=*gqsX5@}"Yvj-E F{DMɕr(y'R,O3JH?"EUX)ȩH)eTģ w'/V\Q}R99O #*vA1Qx\e $HpLM=%j79"V6&_TP^, Mr<9;/nn#|x3pU8p 81)>"9VX0@NSa9O7܌DO8Xl<1Ǚkx&T4odz-_>16 RXYTqz#▀QCeo77(ғC:C%eAB:L1ZV,wƖc;MO)yTONU &Y.7d{Ђ)D9sTiyeSG#c`͐D51UZs)DyqFHEʬDz⃴9UG_$\6!ZwC8&#= U}yPd$';)zR7W=,OI9fd2f*q5,pL]c1EȨ%ϟplq{f~"1z=쉍+K8  k!0ĥ (D LH䵏Js>s>VBeN'9V\^~9$-NP1 ʠr$w2jfʣi ヘ'R)xM sWHv0PufQg_A;-a$Z5WK,38:4M-{}܂,+Q^~6Nȣ3r*fjՠDl)Vrg0`;舮SR BV|1!舮D }KdTE0/6(:%IMAϯɱ-:ާ (<(5ӂG9׈Ti2cK_z.Rdi (be6өw+ľr.yaŮbWwղaǀ?͟l~0 | 1 XbfFhRM"Yɼ/+lY$3OGil+Pk|pqq{{_!M,9o 2_<O/(zㅝ\mak)*Ibs0`,}!Q#m+`ɥZێhz5%] ofgi z@sQ22D8:֎N}~Gw8?"ZpJcƞ%##8}oN)L.QcfH>|pG?<R ޲Y޺G)Gfx*6WMJ)Zpzl\4;%'(%Q0{/'ʑ*G-? mKDjb׋-6l5!ht &_f*v{Bd7#1,|8fϣZ[4 23G `/߿x<^_7_Z &ܒsS,J||s`ߵ%Z)r xшհh[mT; o\M% )v;cûG)xq<3s]M 8l'LI1Ja'?!%^b=XaU@A'g;HbFEmOG"l b`'Me>j;T^>'ܕYLǩ'=MH{";/< )w!S}~ `uHD\d'=c2(J#g4{"z&|Esb2X ӭoǯӄ7*K]㟅O=3I{'Pn !Cnڌn=#}$B|ptwC "MuHfG1REV*fU888儷M6L,&Q!"ؓ\ꨌy5KzG(`f'±QDؾŮyThij埗w@5l[<vϘ&1SuO0 AujǖmW*J4Qu?y;7sL"E/5TTvdX3ߦ !1ٙ*yN<<>pss7 ؟sfDݶGߵ Mo13#nnY2:t@ڰwN3؄$H%Pwѱ ROe-PV/o3`ߪ0)wGE6J]$BAI  gOLfNpϳ?93OMȃ#)u nM1 i5M( u=5d$BY jԂNP~!3d#vDDv&wWa"M)쒖WߢG*QJmQ2B9dQ^v}GTsף8fd`a0UӍE͋QXO5ԙ/l}Vkxqlozf<`o$ }ڷ@)T>3T%bjr$=&>m|_yC{ bWXriְq8;g/m\7jر xbfEJRS ޼{_|5aO}kM"<_T*jgn+mmDҋX#]]InBBVnt8uAHnF!qi m ;%S-L\`fL Nxa-3I FŤ>#žg`J&&f/Ʃ&(aj#{+tWn%E޽9LQ6)Qj%dd/`tӓe>oNKV%GdFbGV/;a#KorկMq^x]t}O@iҝ6o:kq }:sWjdT(B }Ato^C)収C?(CEEeś/nowZ$u"En1ZunOMb8f4Cݮ"lw[#5h XlRicRf{Bs ?RQip(k$Awk\::@5+4:&n}A }Zk<<]x^CS iD$Y^0 n𧇭(";݃ PBDb X.v=_(ŇS65|PAT]KRVcʚ-&de]+GosduG6O-B»sRPbRabO;>[_ lSex*Hߵ,OW* *YvmZ'V.^AtYٮ%NdwfcB-w G. >my|t^bb )m܉8AKg*3Ӥcq ^jI.&T8Rx/kw@C p 6;9 V&e_zsO5V# τ8SG+zŞdbVC<{xggg2OO|7؇EtSA*>6/ED+£.Lvˢo6iڪA"z@D rp>N19«0A & )$=C,h|4"BEͰNDӽ v:I) ƛaJ#X4+EM)@Jt} 'icڶjǪsؐqH' ܬxtެ!J`MKa~(_]_P74ŴBd&q̫0ܺ׉|RB6EOdEɚg X`A*[SLTuOiJ(:8p+wxKBv9ǣgSAZ/Dd!*ܕ!ߊjCZ9KdsFeFgmb5RfJ\Rv H׻rH, exOC@.E'877$"tpܰ@445Kzd%`e,iT¥0Mx6e;sME\'O2sCEBo 45Iļ@B'k` pwyQ M]IDL~7̕jpvf OxagND裏rT?)V%$iG?HvqpOZf/-gx)RBjQI.", tDNƒGo4 /&fKfψZ< ҷ]kroXh2MX3~jRhlOx6-ZW:aGî=-^:2=:>$crM.Bg.64: %Hf9,9 ;\8!(_(62hEpC(X΅ZڹS 5*/9Cjr g~X@j8[D'#΋gRG&|&UW:'/𽗟p<78L ؤ; ^.Zso$*3DcIu-Tc>O'OX'caᐳbe LDB><>>$>gj⶝ET;'LHmc3ԌȢ#%3&BD#3BvӦg298o>8;ɤmj׹禔D*m0N3жMi }:v~ ԉNߵ-vMdӵM= 4k?< }QKk>]C9MX ;v;yҊM&:W K!lMjCPI~yI܌lWl{eS*ۇ0r,擑ˡdJNg>Mgxxxė꯽mZ!ѶꝂ޾5P3q~5{ٙZ'SA.&V'.7Y(%Qژ~^ZapQ,_ QMM/a7DeY2K@Ux||tK>!hu['%Rn1ܦN RHeEݡ uߓζh!%h\nLhd2j䭉ֶb-_@s`zTRg$ĵ#.'N~ϲ0ŨWeR{1k;֌%_:}=\\JIv~[[/ Ɵk(6$鞊=wEܟmp.]a:kϟ_5Oa8>;?T*ˊ1RrTn<Ǡ;BgJ)yypն%BP^Jxx\}PN /[R; E/p0ϓ!&Gʕ.gΉk-*Q睟mX7~W4SzSY I@b룃1GjC$@#iVn6PJFXu6 ]T\ : mG_!b$L+YFq#fBu i+;|*bm7ysm;DTLyQrQ mӄN?8/~i,Nx r2ba? {𢵥v9v-[ij=Sx7OSAZ6 kFeErga0nrˮ{n-&t7RvHp[-snpHmŘ <4/@1C+D!BNOC.ЏACB ]agG05rT)a,bA:kP@G;qklF=m1N{8 QSyl/ ~sSNJPh pf ӆyQ8%mVvE syõlpv ;sWB_sÇy qdۅZ]YWЅhP-~g\x_yz_ӑTNMFb*tD~ [\"*ŧ+N~BHyRm "V I`d-*=Ws3I^o N7S_h'XXSij`hs{?Gܡr<kfRJ() .nXl4"uIZ=þD vq$<>>xsB4 6 c]'R¹mN3Й*M1zpS 2lSqG):( r4b#R6gg{)γxwgM>}񈯾z //,.?*>O*DK;l6@׳Zk}=kK)OSA:}Hp:#{rpZlF.:K!%[V2I.c-$[s2>[W 8Hgг[ Xߕ/FNi$ l-EvhMcZcf3DUh@2g([}^l~0F~;ϐRb۸ in چ^IkXdS4nB1>yLa,,AnǙ#6bl)q8it͢׮ 8EmiG9_>ծrhFk ;;hW׵W8x}ahT+Q^P׋vYsI)ɋx3hm0 p81xv?;#QƊg7K(0TQaYJԾukDplI Z/:dXy;^63ZH&" KdafǰZA)%D\2U{I{ʌ,>kSRkr{۴a8! }uRq<# ³gwGHK?db"^g7K[lg<ƘƘ4|_`-~: Ψ)|UDuUWndo~< Sŝ,ޣnE^ ?Dmyvo3H)qqqnh/n7k/Y|ķ 6E">vaWDe|`oRmscV,:9ZA ljai..7hpm(5.̰WFLv&XS Rhl#EU\ߙݔ9"ү!#/HlB30"# uJ#P{%+l܏MӔ1HoH!,Y#b0^Ƕ668`<7Il=CVٹz%,鏨ޛ{ Qڈ]-,Pz22f@˰la!sB^MA Hi}#InPJqƘ30?p=OFkDSD)];LrWc=jmDj&E(=Т#5 Zz)x$S2~N䦑w-H\uS=iBץHhnup{kA  PM`we؂lFa/q`\/8M9}kiZ( ~O&G)ڭ%faqb 9i᫃$+I)u:eA+d+A.`T{!Z@i)g%s*.i*5zmvx"Ϟ]@ggg)J)|0 #~Ài`['jx/cڶ_֯4Z{g=|,CB)&hmDm6[5; j:7փl_AkHlpfch6X:-Di+:Ju{$QPP~vBsyXM# t&ZuBTnC̙[NHk 7 c8IQs6R{LffFO/zͼ4T$8{[NdVo,ppyZ6wxE,Q2MSA.~*wEdɫN^ACe+:}`On8L<c0ύ1_{ s7 հ(n<1t-'ق^_^^߿o}9}7o3mΠq- TMH<WE6$سQWW1;RvD4D2): QT̳3%'[31f )$-6>B9!p8bg<>"(G9 ^};"n"FѓOF0 (6LӌoÃ_SF)ߦ>(p-|_C8ĚHڠ89W٥TbnRU}3\^^k[Oמiq%\-a›M00>ˋsJ)|PJa&oy40_DJ?"QV!Sb>E:'g+aApL#D#&+&3C<4@]p1M5DEډh^ ~h>Fk{mϯn7\:p?R .'B.ѳgg{a@25VY0<=ޙA ÈqtJH_0!"luBr!:R]=q{"88ƃrwQɹ&.Den'"}ѡz(Rqk(ll7h={!orɆ]#06l6h&y1_.`^}Y!̬ kkq3?_K"|x ҷW_aTvr_rwUr"3imFZk{8Rw'_^@+SbٺJph&I#GuxB4ŒS0J*.s6̕g=dF>'w ! !s 'VճƬ-RӨXE>{~]WLLY A0H$ V,Ω.4(ϳNMC6G'sy"pY!?mζm0 CEȨuƓuwwZkY4ڷ~Teg|vK6}A!wOjT{A?}q[ BzK;cH0_1|OX|Ȳ~ 0DŽGVZǐ{{o"z!wݝ F9>6:qqݱL/s;߹`-^BȘ:>@/8_`^l2a[GUѺp<:{13g/?k0[} ?4e=e$s"ƀyaeAPPŨ&/J*[g=#|)Q01 nnn!t!^~󬡔0L.rT IDAT4͐B *NWU8md G%k?LD4XtOWv[!(ԤE}H7i@lΔ=5ROF+j#qs|?zen$F<! i/ҔY|mwTeCKqL77Ʉ݁F{S= PcGoj!3M[['!Y䌻T9nac^~~4 lxY19Uxp)fN"Fpo6H!7|8?OǯN򧂴]tu>u߅Cb!F1<t*.{myXA|,r~V) {"[R[WH":)€shE !i_2%ZFG CNOg'nTD}t4;HqF_3k Cs[Uz?,w|3K&K`y+(vY)EhmzJP트5w]SCC`g*qŅͦvnA/?Jae]k!]<#<'m;obAI5(⼢\'BSA"N.ZЅ%-0Z`-R5ZH @`|QgDHQmRB鳞BA lp r=B㴢*ŽfN`J;<ѷEnSR %]""!eH '2ʤp%X:֏8M /I7FBᡷEl!2{N/#,m6hI G k-囮in?ܡvGΕ;ȮQ떅W8Wm[fQ`,vАЮբ`X%;*jTm΃mDФa~c'$QFNhX j,v[t\]q8\F{x{RN@e7[@gA]ey{*=CҿW@JO!FDa.,e)$lB)U>$C~' | YAa-awEZkg\8Dl!ܮȲ-v6҉pk\44e JS2^fGFM=^r=np#Ӡ2@f:eEEzI UW#r J]ٸd\Q(b~Joc)a x;a}"7! _Z*. X;ZC.%]lo9|QjR 7~%VTJB)ҴT_a+0"m/~Cbsv>;# cbPL= !-^(:0{Nm&YքBE}Toii*qxc o^kE|lg.'z T"6Y)YۨrqsW\꧵qvKDh֊,*ijd jD|fɷ!uﺸ"^!xW ѿ !BJ}C7w o޾Ǭ5n\l˻7oݿz:.HqdOUs?ɦ]~y`O )+_wem.]DIjΦƧ&K!KL;%9',iaA5M`gNnQΉa*^)*NJ(rrmh^?fb% ֧A4IzLH vePJSaA^L?6/rڗ<`3"CߣӋlNeA}yI q똬@׷h 1p05wxlx ((J*x WW71V .e[4a :$Re.ybJQ쵙-9 I)*wr˾'[lIrD YGРV+s5é=xEg }/?f9ww"4{M!ƏCPr8e'FzZp.߹^#x #1^D BlTڵc./^4fy(YI0k 0Nxny~'}Gg ҿi 8m_,V)V&:]2H g8;۵ew`?! qv3J b2^? q1r4!Obp`^,s|)dd)$]AQρmr07"(Xb~e~68kz#\* o&v4 sO.x¡;'Z,NNj`xh7wQ{d=l}ҾGXfrI$szOwѴM?Skpf0nA74*&QܺBHMws6y֗l0kZL+ky7x8<>>iv`f}8<O o4ܠo-Z g_0{4GkS(w7C-hbt&Y}Zc|gD8 c%[d8V(HYa:-bԸuhqy#{<~Txxtc6Q!9&$|#d;o~C׵zv};}H9\}|ӝ1|wB;M4h=M84Mݠ{= ixx~*^s++Qާ.:CQǷG{WgOt7ZoB5JQ&7nّOEw8+3OX )2f_yTMcj޺=)bq93u'3af@ #M@4%Gha¾0 D:z`puvO cû,YVb(U`QiE״pa (R]ZLevҒZ\pyyg.[Z;I-ۇ$7&BK|&mmq{simu8n[{9Ϻ+RbU48EG T맔gMw\7aOqaahW>U&#I-PN\(&|q?naFa̭R !T<|TH?cjHłNęՍMKWrVwh;| lkF@ )<%z".?u{ ,$x0 4{ wj-(U9 46 ;н"vMiɭd)&05xݳ=6 3\pIHmВ-'[S4"$U9c*|.vJ!8Tц]*6:BDTja턊"ƺ4: &ѵl6vnRk}9 0O?7D9S۴o01-O770 qӻwiwo5y4y*^mB?o-FȊUd.ۓZA*Te!3Xba&Cnb82򷴎KW~5{MV"@c?yUC"%H64&g muR@=LxZqaϞ߂Y&ԯa5k>tޯLY+:QZMo wS۬pv)ùȎs꯵ٴT c[LJ`r X'ǿA0 Rs30=p#T 3r q+zBK ;5$#Tnae6@DB9l,4w2yhOsW} 6;ʋ"vw%hDX0rzDfsOBÇ#Te. E<ۆc=Ϳi\=@׶GpaDE$/pELgSB,ibi5IE/4!(IN X lWV*2 Kqa_ 5Y4-zJϳk3VE7UgmnҙV,_gF!z/FX s89ɢb%tE_Jw:oj IDAT uXA} ?# !B\n6lu؄ȍ%DH>1: 1FZcx[5O;KSJqŨZVZk&"荵/]_OnI7+͜]V _'Z]O?9}b&0 @h}t~Ǐɓ <~HLڮs;a4#K»6ua&4`y5 5!?dlԘKhbaFOD0HIB'y?EٕfFcI &t>i,f%m"r$~j5y8_~R)o?g@vS|d4vi))k`>EgE`ɀl6[ M9 4UJ^mѢCv7{YQN kv<5@Cݥݢ_X!I9viAB0?B/fLOщu!!MSIɷQN](@L7"0{(i8zqINgĢGd"(ǞO6r,#Ibkn6-nnop=VUss{Uvzݶnm;ԟ2U#FK *~N&!t&7]B>0/|JlT2ͽ|Fb 2i>Ťōf:(OJH)rR!- #,0{cC]lvҚNT?z&z/qBU3\bI #+c,l'p$%I8yqQZH1T_ V)e\ެ۷1D-&f fe,P"7iҘ7 >0)Ebj\tDbpi{>v\:)r0PaSnYNk:NAkB4IuW|aHzws8^P9\=2l lZ12Y\y7Y!;3)'|o+)%!ڙN[TXĪvKbk_g}!$ְf t(α=X%".(i~-#l6_~vfy8nNJNCj's䚉MGNH0%,3m>c߹A0ސoԚ_[? zx4!2y$'8e`{ R$tqK !T7~U.n,]l;|! d |%!1`xz} M0E%\vF.A4l7ר ?^ɺŇu]7UUUmpPJQJm5E+[uW$N0(4Hԉ :Ĉf9 %e\Ӯ,hf abNd;s2XJs0?~|0 3ͺsv&_‹A"X9Wm=T]7(\G PU5c儖 o!{D lf-DD5 6J] ,N2[nL:{]:6 <ܾMY_ʲp{1C5{DXk{5IB|e] 45'*eqN6|٬DY i8/ΚA;"t{Ģ:뤢sÔ]GOtk7Bz 69-dRJnW^nKv[kQUus{{uu8 íRa kXu~A)=6vK*Np#"9',*J œ9%O/x!P^>NhI+s J@)vwxK#Ἣ DKE[>-ȳ t}v [~0ӏ" h뢁fEY@S&rzZE ن"jAxZeV򚾷4œy/* G/a6z6.^ k+(ts,K]i@g%X7LU˅ׅix~FNm8$hNJ% hh{pMۢk{T;n/.?\Vá9in~׶B R}makKNJ,w iGIpԶ)'8t9" hnbT#a* gp*)sscV3{(U) 1trJpY~wTGL7ųș`TfkD{-m81>g"rق&X@ť*6%u_:EWĐb>׿~٬}]֦]ʹz㤠 ghJ - 0ŵQ&lV]NZ3;;sK㗧E9luv ҵ, f%ch4jҟ٥:R ڌ`ؙ9l'`΀AJH+ zߤظLxQ5\}CUUx7x ^~JyFTU[8 M]RF*';(u)e7]x~&d/|h\u&g"bحVmtzw3A˳40S2~$ydBZكNVN;\wrdLMmہ s} % cQUڶ e;馠uZxSFh8Ny3^IHO顉^1p :I{` yqhX`1a1#scf cBgr ZFu#ʤgA)I П,3nUgJ &@J+`H3Mlq faX]=a~;(Ui/_毟CU'p Lxh#du4,xa;N29rbȎcn&xO H1| e~/Â7dR>zb-P,l\ى%Ԃ\)LP9AZj&̩NRJ`L"R{+&P`\>$Ch9Q!Ke5pU'2!(zU'NMﯦwWhşg57e+M(tÔpR@댿 =5yzpiex 쩢=Y"dm'zr8#봜bZOW$8D\,jnz/rrZoሮmC۴6Xn~#|݊RR^`%OLH F'!Z;7)iC IaAe Y&s|̳0@.*p9s-\Xkw4M {*|^B_x5V{ͭuhIp*0c/1ӃwP ~ MB*_ȑ9>y/acXFG(е=96\&/ A4}fI$Ḹx,p~Dv֙:s<~ggK<y,ˀUG6"EJ=Et&( ,Q49-toe={'KXby=ݓs?c_nx]b/:ǙmCNє3iG VD`y:w+Ȝ73dq<&vdch-!DSH>`'>_?G0xVJ(sM=9/T V#aYm<7jY8??s}RJ(18JK ad*nb<\idtw4|ja"T+H:3?WLYXXWe2AdV7;"7g'$9D۶rP("ݣz2L>}nLd6/eM -i*ך#&b e.,uBlNؒTMm0ty'%C+v;uw<pu#ڮ=t}B|#|3HF)4][vHS)7;$8UDtiFL>>js)`,csyi"-ԉvٯpGAG|M1rk>-,\)稪y}bwz ce$3%3aГ?/^C>5"]B3ByrOpetfevVxPKFǔ6gLA{1 W(LSMa8N1ebH8yO9OINH`<_ ^J1f};v|>|>s;v$ڌ { 窉ɩ{ꌠ@vAPHaIy,B$ጨDMo0%ML9CLYh m(2n IDATkw{ݭ ޿ kz뺮n1 Vw X+K & 8"#Q()3Y}DdٱXaT:u'] h wH,|+Ȯ̲lIN1i}izb$ SzF-{oYb}S 98Zxylƹ]cE8>z"7F( - ; '뎄q a$$fO~~v!O!#4/4BRI4?x-noUݬw0 o;1R~Rr7(\)pPgQa bKF]3$aʭQ&E l2,{UsGM7kJJ!h!,-1ùe;l \22]ɪM,1]ݑ33 xdyaX.r>G9ڦ5!0{MNX'\=r'BbMO/[j؉>1O{aTdE_jkjgRFv@zI]H:S-.7ZGOvpKNFntUM\W'Bfq1>j,@B21 #Jlw;ys7+qZ5wuWj_RFW`R8(F pN&&SUh /g1> Qp؈qwfV)L$I9OmU[+z){=D#D\>%Ƞ($`blWukG`@'@`e7ؾ]P';Xs,subCӶgmkLx0x28P)|L'SS)$]rCsY9CJ$fWMX!O]]^Cp0atTDDf 8I1|̅EbƙюEo^b1|>ãGgxt0$ S\c]A cRҰ t &SO xS@B$i[WkW5ڶǏW8im47uݾc/J)u`Br[NqTXG!*ͣ]EiF2Kة\mq9[ZhC`*"8m@ 9L)HB I5dL`xes͢V{M7 @.1ٵ>Ig%dP9꺁RUxnB@vu˞\Z<>RwDno7J$'YDyЧ/ٜ0u{a=x:dg˺3()ƶɄY$d&Ɖ9'wS̄Y[!;yx7}:/ +MG u3wGTW>!"',>8%/mBth^nÇUӴ~B ]V He\ mXt~qݩ얇r7vpxLPD 9GFrĩA)Zц\1HƑҽE3;".U"똑'ci_5ap,}=ڳgO 9~+7)aQӑ0p:D(^'[~3!`L[6<{b\.ǣVoжvDPFyjg7F3&%.&Sa6:(Lau \gXJ}SY׬u޹p%C&!)`Yvb[4EQf2?u&->4#<:?B;ħ&@@;M[oM aJW:be9sW[Ɉ1d'z$42bMd]Wwkv{Vw8꺻^7?J)n~~+T`=қWn 8-scáG*uʒAFO<2`k_~E6L2‘' ;6 = L=EZcVKxBR$'fYGH%M?RӘ4"wAӴK:2,JMćp'ɟ5:] giQYNЙ,<5B+ iì,s6Ϸn;k|cp-Xqvz"w %PQCi~(\6.\1⶟C۝*ձBulXVolMSnۯmv}/bI6[ ##'58a;R7`vz/ R}›N|@Dp_\,)1qil(43i$ sqooXȚ?à^DqTBb~Z1<[syAאҐ6д0D(DoU8="^Bsfz8VIiZƮm;*~TB\1mD~=IZ7r۠EN(Ɖ ˲Lr{hgΰӈ9 Mb{Q n:$fyY% '+SEXcQ{r#ϲ aa>+KwNǣ[ˡ,+t;g۸Q[~3Sp7Yٺnp<qyyk;݇ձJ lx tU;NLKFCF{irg@ P)>)N3ڢ&='@H)CV͂Of7TRɑY#Y(` KQH<[8ŭY))4SdZU}Qj{|t*1؏靑b>ógOP7ڡ~=A4PuG}󑣮R㽈pԿ$1,ܴT{$[ދCT:#(bIjYnO_Ng8 (q!L&(QUXЛ. @f7IZ箈0H{Com5׉zwp,sEbƒlA{A:Z I{*"F,!ISevsU{m[G{A6nluu0R)={ = 5f/J"@۞5Fhr]2*Jߵ_sC:VUL L0%ة&H s,BQU(3EJIrwHUxHj3Tgƙ CbdHbFp  1p ͣ(#⫔g-k] u6q`N`U3O-B zQZo`(܄ms Bm;ZЅpL/N>>3Sa+Fb~RS+՞:k4I5<i Ha\31a^(rM4#NB[]:0j6bѽ"Gg˰q(٩Ų5Pk.l (Bsi:|>w!޾v+GqيT_A~/ +6Z'DT .46#Hf<:_=ӥm:=#]tJ fR/XR,]R> 3{h/3+ED8)?E=NH"{GE{E:^.fwÓDvGU7{@&V]ډ\OMdz$z:Eຮ&t3ƴ+<'Ҽrv@t8+Gl6[/_a^iд]?\I[]`k(&f1ԜE/8v݌ )^e8$ >$~I 9qOL'-J>?*XRՉXY0&?$WR!tSANG,jOg2n$+] 2;%9"R>zq_j7QiTs?fc7s`J >?S!CNIbd KRMu(NBaZ=4u -~5 DmЙbnG2{)%:dً c19iqrIh#C"W8uGб$tsGYXU۷q8qss+}æ~[U7 G)A) GN#ix^kRWKC '" ݍ4lSf9. ,Ŭ,'{[*`wnxRma 埔B%)NkdX"]!hFe9 RbYZ&+b;.40*4&;xQOHB.S>NM& _(lZh0X|FV4 :pAY.x1ʲDUծx)B y&mX?%LLSitpU"BtD&w]jAL5  \_ /_LZ RnvG(55A-1kkfEQ8$P1upE8MX:)9J)H6uݠ4Mi^ߋfkvîR]/G V1pm|':MIz#DlvWʔ$y} rb"+t0I̞|DvP&O&[Tb1Wx@7YSJ(a # kBxQJA9?C}oYB!,(x ҔJ΂Ɉ%2V{nA%K+ixZ8'AL)LTY-lV2O\NLB4W@S%~?5kR;Rĝ ]\aD(uWJH 5~H\!Z.Hy:⌘MY۸衢HQԎlHtmW0g zrW'ŏSf޽M wG𕜞:ɢ,'nZ) #z*9¸eK&'#"M4ғ#M3JnK1B vh?\vYuͶ7mׯۮR=`2MĨvRR:i'T5a!QV{tBGS~; MOJ ~W2^l8'^IF{-H~ioR7`|?tmz!'2;l18OS~ʄvJhvKlr1N; )AnD#^wJI:h6c:`Mi  u'"2 %>Mܠzt كAQkkJRoY7rR kqTfQR Ibd QPmvőU4 x4!/ C]ڶCv(g%?rO$|_n^u^ޮm{z!V)4ږ87L:!/ IDATRx 0:&TRz!3p nvDc]R ֯AlL1W%?k"CUDLP$|px^Hy줔Ada ܏irܹiwʱEc]< Xε(6E 4 vTmӢ*ӎzp {c"ŧ4vsS !&{L-&)0a;/\bw<G ĜKZeb4(,g0FUոv'oЭwM;Q`[#p~!逥q3HSTC:ju@ZڤHK3BF8 '+ "?O 3*><}\s9vM+D0~GR,8s?7}/\I&̃x~雓",50p5YI0X,H̽?`믿~o޽Vwˮnz1T]/QZjQ%4s. CzDe":7R2 0F* cVtTo=Yy2To~w X68 fA2B#]@qDCc"7ݵy`B{Nއ Fs.1M&jC` 0Q-{,c!X̰ϱ0vQya9A'Puݠ;|˗7o~>^k7f= ߘLcLB=a1hXM&bJ2ӓ"Sb %& h'"v<RDlc (Ggݿ焱Kфqs1{Zg+bqF0a@'ڮaHg>j0 ;bKiINHG\XM (8D`G?q}؅cHȝV*I8O8Z k9M]Q$4F)O#mkYVݎ[pwD1'jjBו$DrؽzbPml4t5<0(gȲ,0 y_\:nbv{|x-?\~7߾ñ]UbU?ȵ.>| ƺP?dW$X qަ2ϊp'R=b̠ɥ#1*!$X݈a '%NF2@Te}.H?6 r y6HR~ GwŇx]m,i&I¥Uݮ'۷U!sL)&0d†GHa/C%ey'sdavz/gY6zz1hcCg%X,]cCnm2yMD `臩Ĺ~} |hR[#a)6p sE'K0n7|MEj4C{Cb2GHp`]:3MgYYYl%M#S3H.X e`*5pjRK {ǦB$AJqVSYyLz- E)߾LCaT4M9P鯞)dǴ˟,Nq=#X嬸z3t,Ki~]1ɱ0@`pXj,;C %,H$tsR!e-P99ʲIPg:ҼѶ-D]7c7މáwp~_Uuuh$xevC\L#`Lw9V@7ux:2<%n9=γ|r@E H4[|#'5SW WcTt86S#E ZtF@nÿ 51tX. S)jbz`3ɹwb1r^U؈nfY3Kl \]1>3cДX/sƣx>$Ibd!Ąsi(0Izi9Cu !50!m} ?1(Hև,bv`E*WCaY]l; ip)qh K"g 蛣,K;>!=/5ڡ\ʲD l;Gl6[|lx8Th ـ8ցL/vI#`\&% nKNSRŚڔwъ١rϓ""6 tOl"\H#ȟb^my1 ~oxTUlJ*EqIQD$I$SO4-ߦ8P}ĊCJYtrjdCP^i+W._E@8{!Āi!Y ĠWAr* cCH-7EUƘK=p-,~L_iY;YJq͹7RRk֬}sSb"q~DY40To [RBӴ]w+TU%߾Z^_vuӈc6BU`L"A^㍦%KA #qYPf,@ !CӰ l]xz8e1:" ?i<.&D:=Q8Ϟ\};\$aC\ɻMtX=iV20e&>#UmZ&f.ܫ< ɉIVȤX @1+>f<R3 10kJw+\"IA^9AbA +HYq<h͟]יHᦃQk[`""([00ӕ۳0 eI 8ԽvK"όAj pdi!nzOQ$o4ϐg)Ζ( ]sf3, יo;v+ݭ'l6[|xݺ^Ci ̰SFQ)`SW஛0c BgB3)^ k(8>tmavj|RgXytxdzB@bY9gHA}6?K*IJEJXn3H\q"L<1b֛$:Rh_A\KҾr0I >OQ%$ ڃO8?2מ>"c93#F2Tkșqܻu_y|(w2yDtqq<i@Ji{fn{yZcUth 4VӶ]D)LeL;FQ !! X5p11$%68d4!p)@Nw_@ sA/k%w *N߁y]]v ݿŕR\0l(m < I{PjhHa) VtX Nvg+ͺYxq҅,>Ӧw0!bcA_ָ@8;=(I& m~CJ$'!4%L-:B( ;Żyi؟OLf{+ ˢ$]Ԃ@ B=f\Qd,3erju+}f)~h`1ѡpSqSwc0OLT&i&9cPIE `T89`d hvљ"Dg }G2 C wkXwM %!R֌hW<9'6nTHau-D_>zE=v KoxOW<@k↖ ^E bc-\AiH \#i4j~A߉!tMQ99U΁4˫j⢺ߖ%կ@ng`бs?:8<ȵ:eGSvB|̂MOBWB`. hL?9cS>$OQO1 cFP.Gc@g">:C o;w)5)߇xfz&)MYwf.}p會%zIx![@R'ZXs0R謷 yboP%8/9%w%YKZ() #`=:,T?VȺXE2Ƚ `[Tg21VF@鮬/5*v_5l;x~q6-nwsC80/1PXy%Win1pm,C (C sPEm/',|( Gb@9b=k0N8t*y4V?+SmT3 wFO7`7V%$EI0;"߀4lXM D}]ڦE"xwKvIlBy|2Md|40&h0A #40ˠH/璐X)8x(`\!(BaSBwk"o^N{DTxkPFoe #Ra˲|#E7Dzp~yU?U[=nw s}nDBnlK+H,l S'cHQK!S; 8*J0(PDGv5^ ;DR؜N0Frϰ׫ ?Ι^9|[%swSd(۩. & -lۮ'Ww>Ofba"MuzIm# 𚃸H!Y?Pla}@c툄X /T ?/:7٬O D) OGboQV3mϚYé:;.F2sf٨;;{ hZB%~cӶx{rm۹nkDD ,E.OCQ2,#O u}',BgEj;h)2C"z:bU>Uc*76k#Cj%L@&2e֨DZ5,縈8/Dq;^N0}F"Dm_ݞڮs˾-wgHtL1 Ճ êȕo^҄j.F[ a,-d&'d+ ł, &E<+E =jWS)0tP$Ϥ1d 6 4SZYWBDLY $C ,]<~~6mW,^ۂ\Svʒԓ؏jv>` O`(z`|vn-3׫h|7"K q \X#,Y4,G'sUrQ)7G]AG7 +F IDATd*5^cX0E-25@`lܤ,,!b1w} 9,`>î~+?/.9Wյ{¶sg6 JdP"(h(B\D@1(%$T4Z^F|5^bT^ ;,&U.(x -ǖ&3qwAWK &QF9|9i p4ܳ)Q[Y$Ȋ  ~lڮ]כjv'q'  e)< cd,D H6Z "WJȈ!QڌDk#ȃ£8=>!r7k`DՏ=>6=&iюq_VScVV~'ؖ~ ԙVwm1abMMIr71P+`zC$+YX\JP,I?Ȩ$`<eYr9<>yEYo 's=\\\zvUֻ5mQ;[v IJ73q3ڢ@3ڤѥ_SGsj__Yp0:l/0l6[X6x./ oWkl:M۵dF3PXF50 B "0. 9.lTH%N=)308 BR +ച'!.`檄iJ̈́_X"~^p.tj`j ߹ab3kowOsGrH]_f:*\ f |..k^ ;AiV0Se aQP "2B{N$&1[; HtݶEu0͂ +`( zQaܧ!^s/H8Gö;p?mUU /ۦܾn\۶شu})Ѳ@4Rb;SAO_1rF"$`yω8婪ܘ "rDwm@FGV&ƥ8{SW\^ qyu3yK|(H}LN:}H' MKp\no?g?9+C"׻щEAQJ E8Hw2*ˆbH / "@~Ή$ .<\T&X@y2bEK@? Q1&$ g,wpchaA4O>Ŷi Vm9tЎsBAFnCcNjD ȁ*Bef<=#CSW" $5WYHG c3]!2ZBsHB DlHY/JiXL͜( N?)#+S":dG#"I#/m^=Q[o^ޮrZv\.7@"vTςaj^l:|䜃uY?oF]RC<I* !U Z`c $<"O}u]`Ri΢(xJ2"ђEYBa  =X,qk-\x=<;ivvU#BUι{4")$^owߵ~pb,vC ϢI跲40X{^dvm@O1-O >Q bWd+Ѩ0)vT  nGgn}V 6ia bg 092ж 4M 77ujf on\Uzumz7H3; ܴTO͠jbKTihCR0cG`v!5EDCp\Ql =ʨ>nL`*0zYpm|@pxA.g&8e6(º#p)yR&> zl;o7_o`L6"V^>P)6$AZ:׷痟~{/.ooib=fq )F;| GQV@#thNL8s”) | em|&v[]Pj1}(xcP%0 s;X[h_޴UUju7*״L!:hk#Si}(=)ߪ=~<+0q{^\1"P..j%=70r`*I덤e9 fYFYf ;RίU@rhVn6^4teIꛉF$CQΔ7+ s[}(֛n;Wտ'gOw `ߧTB r 椀/x W@Q9!"hFȧ ` 89"E C! AJQK F#灼!B&1_ArLM{h9 .C ڶO> >kzV;qqV@6M xKYgG{ FMqibHTFNAsCJP)[L]]hY 2"q  Yi  &!2g7 τԩPBhUo! }^4I1_VG.BDH((d8=v|nC+?n>m.Wِ3 >]u7(NfW{WYk?zIH.IN%Y{sq='^}Ԃ1%8n?ry~rf7 x( @, #s h tXƴS0 [oݍav Ԡ  S-z?FI/;nob(f( (hnKaZO 'bpVgpt|GP%ڶzUϟvW~P ^imlU ޣʄAӮ(S'pV&ɇb9Ug/?r|"޷Bٟ2J0!zLc"5yhL'M(&BǍW}4M )_,'76чʻE<Bc|QoVr:A]b.>7_JTܳgoP>8Nh 1Dw®ȄsHhzCS90 Img q} IQaNYo'.~ d((80$xQQ.BGjL4cMՅιC2l 1taNh:Au 77wnnnWmvvDŌƤH5a50LZ!$a5_MN 5^i K3*OlQP>p*Rw!OHd^;CBsTs'"=ofҜd!Md"\(#f:d .><|j`/v>v,fǡ.> [MlV\ޭVo|,/ h(U?[c !"%a֔19xB#e#t\MY3ʋz#?dGeAt8|LЏH3װ/jꦅ~4$0ȏn^hq%<>=|>8j%l7;h.//ϸ/7[\oqVj#dRzDv& l 82/5t[qZ>~|ggpU&|0Jc$aG܂[ڠE0@  I$ e?&%tP,(S C,2*,ڸ}%\Ȝ 9m 67JVDm оU?qyhفe*–u~dZɟ___^] ַF%t]}5u|oIrSxOQGMĞ'm0m\0mxp_(dNBPxNvRy!b*9(, f3G,\C( U]ϟ ^\]z}Su#aH'l[EGwɲB8̂ TQA/h5@ MÑqD%^;* A{ =q DY2"ԡF]P3z^GZ-y-TJĩ><]kbhL2AKGB/Qb & Ƈg?g*,S H87gО!{Z^_]_?={xz|")4a%73Rq=$F(AP}Xʏq@JR^iWnhd(տ7 cM,R>|>Б3y5=fܵܮfMm[ Z' L &8/4Ǐ:!VP=+Վ F>HPb,$MYب#u^>:&\3&$e>қ RmO~R{DM f7xoe.('zBv+U7ǟ)\e|XX/}~~׾G)|P%TU=u=VX Vﱰ!?x' ]{d[[KD1L9@oL1 3  3Ta#^v;a?v;<~Q=պC-8S#s"p@̩@ڷ)(Iʹ(gH#kCG2A%Fa =Tx9N9ŷ.޻5uu`?c; ?0,,Mfup1@XH;&9N5zSHoB\Ѝ!<85_^~TL!;\CJMr:_1^92|z5qAQ 97J^fwgTPQo A:U鞐T<YŮU\@4pjXl*mݴ\_CzTt7uA]Q vdy)v  <-#T4@fR &PWjp,jW-xdGp~ͯ}}󗟞=.wf,W]U١-ھD(5yx: *By!N GCb {6EP /L`TVnvj퀄fDǢcna=W^ v{|~qY]Vv_Um=!w0@c6]Xfj(d! CSRJ_v[4񂤽Ӕ᪲ "Q+`dɫ0ZlID(-2 4.cX!U/ێAcox<' uH3^8|)> / 1y$vJY5%'ヺmf0,6n[vvmk~rꚦ~@d(:4">l-N46$E`8@~c8(yO%#SX"IMnf777ps{ܳgsTuH-l="í|vPPt+*2D1ꯜ<~|Rok8::GX̡1,Ⱦ3\&-K!sBc{*!ۈX8!'7\7 v;./`^o \VljS7m[WUι \cEDvؔLsB{Uq2LaKÙ`*HɅ#A3,*YJ($N} aWA ,>B'69Ǣ9ߘ JsA"?J?B Ccҵ¨`ltB{2lz*.`C<FВ'M F"ڦp>:\uS7>m7779Dd@ GsU@@o!pܵSȱt4Z ȍc>O:7Ѐu]Cl^j뗸ZVn֫Ͷn #2n@B{B܀&&${$_24Ҩ`42͋6OU_p" Xo&Q`1>eH|2(8<)>^Gf˷r8'A[F^X,f((!2E*1ٔy>6m|$$̟jy<ѻ/2"=ƟpojίnVW}h߹_GOyxxprrr<#xh G0+K(g^W&## 1" 34ޓZkhV4M/_@4 wm4xw6mivmsm-9frΔW AjJ0&w2?B]J}qk %$HH-o(縂.wlBdDb;gM.1b=#Ȍ.FT3F^Rc .'.x(/>n"%͆˷KU@(鑎C{~rk -sr#X}/Ol69(̆y"֏9=F p0$va J҇ 8r02}U&h !nh?ݮO? W/]]׸Zmsuw=R`0&`l;B6Fō^N|3Lnlr.^T\T=Ó2HNۣ@O0H҂2MA8fxLERMb&HŜ&xaTC_Ve]ɼeq@  _#%M` g ݣs> x D\QtOlTCY2]P9=%|a솀w;bN&xv3]QIg4G@.ɣGo>yrvztr`\“'gpxxKJX.}vsUy4b" @Lveiw=l;hv 6l[=a]7nm۹zkw~U~ݻ훦r=jDtq.P=FGqv%Hh(w#X*Oi%ɺG;?gɓhyl{)>yrggڿ=;k`XءX̭C)ͫ،E)RrSpnzkۻuiݭ`˗7jUmW;m\CD- TAӲDg7Wϝ/:V/IHߕzuALQ'U{(bjK_z?H *lj@2ykc%*^XCX!]m$Sl] :֯ #(@I I:@ZسEHtIyP}nyaj6T($? 6 ?^c!Ŋ4[o}`ΊtoNo/NbY(rX̭5`4U> ;0 kD1ЏB@GM@#m]}u];Ukڦm۶MmZ\[UuuHH/0eh5^mYTZ@>-&c wWS ܘpb3=A90C`,F%"VUzS}u];׷վ^ a}m` 5]@`lDž*8lӫ0/k<יQ~v0=w7m *k}>GjO}'OڅAx&c)-3S2 ,,C)M!efJ?:^3V pHK0a3#͇U\0`i+gP=>79J9*;&eƀr#bq,r@_0"EŅ~bG U6v((B35>Q`dYPƾI8d2ӃmeMFsKr~024X%6/wѩ<6WN띐"C-ܖ0h*WLlD2O?d szc |Au5-**'eP$-[&-G0R|QfB eEVd-ٟIQ۸Nb^ vo+ Dzȹl1(gRr#Z^W%v-@ C @6҉'dP@RleӔ{V*ѳP|FQM&O q`$! 9o;͘ 113@!+`jS\"؄;g"VGb߹[a(IMz&XS8nf:BΦd&zBH^7)ىzn 8mYd@+9 .|DI4&sSo(PFx1qo e;D g`DH9R2[x@H_b_J.Ye8GmOA(.^[DHQ?ߍD]FfYqS=94ڢ: x+JTn yfwfD#͔QqV <̏[@6'? Ҵo^0>0#ab89D)dJV-30+]Cu|("7TPq ~vjяԋ _D|SvyX"J'"l-eȽެp `Zg:޼B)wģsmByS/ (B .Z 1&&S |`?LycKR MW'WQJÐ3| XT$IDqav>b&DqKh)?-VBՖ]dA}2Aza[=&pZmxtAhJؼYNNy]ZZ:L  `"#i"&Q (rY\e|4dRl=~|s_WbtD45%Bᧄ:F,2aq*wpFOJ, OEf;t;]I4 y@BG!iJQDh1Am SJLԮjqPq/<ϯg>#ok(ajr&K a0=Д9ԚϝSzN\V׻BzSn''CesS#S])HrY.fyAHP)!"ʾRx-a#SMN}z7R;xH8@@(8'իnЛR?f>llIENDB`notiffany-0.1.3/images/pending.png000077500000000000000000000031421352050573500171170ustar00rootroot00000000000000PNG  IHDRkXT`PLTELiq\RONKCgݷ|iݎrګyyXTcgtRNS@fIDAT4+I7w 2p8p8m5/_%] !F|;N,gL?2+LJ_%I+9!}U=QE5"[l8\o bKԈ@&|d_W}U3B*~D\c lg䌐遟4~dWsi`Ֆ5B+BT$,xA#@̄)oNmy;[#4W=OBWW# LpS-X6E cJ dHtUu@a(ST %|Fg 4>dYYՖϲ5+>OV%$,\S?pC/M0|4\J:CRl\ˑ+SrI%|s.I2W e>iZx\mpyr/t5 `2Kju>עrx zoGfN\ӠpMfƔJG8\\qm*/N +np Fm-Xn6pUA%Sj8'nG2c -niZ8ܖmpkr;+Wj8#3ܞ.-`k!W܇ᄄ{IY?=p?& {S)rOƾr$Ǯ-N½ +걛p^ZE;i,]`"t\]2+$:l0t)V%%B[]3hl(3tɰ.{-ckl%WtʱnJY&=U&CW S+]ڮB++: &'躪NJ CיiݧZtmr:tI?Ȅ~ k*갂ZEKK>2h,*3ɰ1o -}ck,'Wʱ>JGr2~z,b21 h,}e<*\>+L3𜾢ߪO) f %>U YaLaS->X6Y:9}d^5ޕdxO2iwԖ15ޖ+AxSX$x,A7a<̄_)cEm[gWa\$OJƦďN +Ƨ0>wbT51 qI.FUL+9%5h"\0^v^1{JƬ^W1{xraLo8DoYjf^KLe>?bqp8p8DOqtA|<4IENDB`notiffany-0.1.3/images/success.png000077500000000000000000000021251352050573500171430ustar00rootroot00000000000000PNG  IHDRkXT`PLTELiq¦uCXe)]#_$j0ԿwA^#b&sB_"x]#j-u=ȢĪѰδ2tRNS@fIDATxYb0a 0dcK d#K-gA4M4M4M4M4Mw_ݾo78-g-,g,p+pg+p*0g*0)0g)0(g(/pΏ^@@@@?bu~kh q Q 1  (G/P]?n+v, G+`,wo_~_~_~_~_~_~_~_~_~_?{+jv]q 7-oX ߬@XQ~ gggg.w7uk ` S S `@+_._*/+.*?-?.?,/.*ǭg>Л|m]0|#/.! T}hu&_hmfheh]U._MnE{:ppk/:pr[7O:ptKWo:pvywpxipzYp|Ip~z+pj/+rZ0O+tJ@*B+7/o\ ߴ@Ha~aFM /._X_T_Pğ]ƟYȟUʟQ̿e@@`I g  //  OO   /@WX? n}4M4M4M4M4M4 1M3IENDB`notiffany-0.1.3/lib/000077500000000000000000000000001352050573500142635ustar00rootroot00000000000000notiffany-0.1.3/lib/notiffany.rb000066400000000000000000000001521352050573500166030ustar00rootroot00000000000000require "notiffany/version" require "notiffany/notifier" module Notiffany # Your code goes here... end notiffany-0.1.3/lib/notiffany/000077500000000000000000000000001352050573500162605ustar00rootroot00000000000000notiffany-0.1.3/lib/notiffany/notifier.rb000066400000000000000000000116211352050573500204250ustar00rootroot00000000000000require "yaml" require "rbconfig" require "pathname" require "nenv" require "notiffany/notifier/detected" require "notiffany/notifier/config" module Notiffany # The notifier handles sending messages to different notifiers. Currently the # following libraries are supported: # # * Ruby GNTP # * Growl # * Libnotify # * rb-notifu # * emacs # * Terminal Notifier # * Terminal Title # * Tmux # # Please see the documentation of each notifier for more information about # the requirements # and configuration possibilities. # # Notiffany knows four different notification types: # # * success # * pending # * failed # * notify # # The notification type selection is based on the image option that is # sent to {#notify}. Each image type has its own notification type, and # notifications with custom images goes all sent as type `notify`. The # `gntp` notifier is able to register these types # at Growl and allows customization of each notification type. # # Notiffany can be configured to make use of more than one notifier at once. # def self.connect(options = {}) Notifier.new(options) end class Notifier NOTIFICATIONS_DISABLED = "Notifications disabled by GUARD_NOTIFY" \ " environment variable" USING_NOTIFIER = "Notiffany is using %s to send notifications." ONLY_NOTIFY = "Only notify() is available from a child process" # List of available notifiers, grouped by functionality SUPPORTED = [ { gntp: GNTP, growl: Growl, terminal_notifier: TerminalNotifier, libnotify: Libnotify, notifysend: NotifySend, notifu: Notifu }, { emacs: Emacs }, { tmux: Tmux }, { terminal_title: TerminalTitle }, { file: File } ] Env = Nenv::Builder.build do create_method(:notify?) { |data| data != "false" } create_method(:notify_pid) { |data| data && Integer(data) } create_method(:notify_pid=) create_method(:notify_active?) create_method(:notify_active=) end class NotServer < RuntimeError end attr_reader :config def initialize(opts) @config = Config.new(opts) @detected = Detected.new(SUPPORTED, config.env_namespace, config.logger) return if _client? _activate rescue Detected::NoneAvailableError => e config.logger.info e.to_s end def disconnect if _client? @detected = nil return end turn_off if active? @detected.reset unless @detected.nil? _env.notify_pid = nil @detected = nil end # Turn notifications on. # # @param [Hash] options the turn_on options # @option options [Boolean] silent disable any logging # def turn_on(options = {}) _check_server! return unless enabled? fail "Already active!" if active? _turn_on_notifiers(options) _env.notify_active = true end # Turn notifications off. def turn_off _check_server! fail "Not active!" unless active? @detected.available.each do |obj| obj.turn_off if obj.respond_to?(:turn_off) end _env.notify_active = false end # Test if the notifications can be enabled based on ENV['GUARD_NOTIFY'] def enabled? _env.notify? end # Test if notifiers are currently turned on def active? _env.notify_active? end # Show a system notification with all configured notifiers. # # @param [String] message the message to show # @option opts [Symbol, String] image the image symbol or path to an image # @option opts [String] title the notification title # def notify(message, message_opts = {}) if _client? return unless enabled? else return unless active? end @detected.available.each do |notifier| notifier.notify(message, message_opts.dup) end end def available @detected.available end private def _env @environment ||= Env.new(config.env_namespace) end def _check_server! _client? && fail(NotServer, ONLY_NOTIFY) end def _client? (pid = _env.notify_pid) && (pid != $$) end def _detect_or_add_notifiers notifiers = config.notifiers return @detected.detect if notifiers.empty? notifiers.each do |name, notifier_options| @detected.add(name, notifier_options) end end def _notification_wanted? enabled? && config.notify? end def _activate _env.notify_pid = $$ fail "Already connected" if active? return unless _notification_wanted? _detect_or_add_notifiers turn_on end def _turn_on_notifiers(options) silent = options[:silent] @detected.available.each do |obj| config.logger.debug(format(USING_NOTIFIER, obj.title)) unless silent obj.turn_on if obj.respond_to?(:turn_on) end end end end notiffany-0.1.3/lib/notiffany/notifier/000077500000000000000000000000001352050573500200775ustar00rootroot00000000000000notiffany-0.1.3/lib/notiffany/notifier/base.rb000066400000000000000000000057541352050573500213510ustar00rootroot00000000000000require "rbconfig" module Notiffany class Notifier class Base HOSTS = { darwin: "Mac OS X", linux: "Linux", 'linux-gnu' => "Linux", freebsd: "FreeBSD", openbsd: "OpenBSD", sunos: "SunOS", solaris: "Solaris", mswin: "Windows", mingw: "Windows", cygwin: "Windows" } ERROR_ADD_GEM_AND_RUN_BUNDLE = "Please add \"gem '%s'\" to your Gemfile "\ "and run your app with \"bundle exec\"." class UnavailableError < RuntimeError def initialize(reason) super @reason = reason end def message @reason end end class RequireFailed < UnavailableError def initialize(gem_name) super ERROR_ADD_GEM_AND_RUN_BUNDLE % gem_name end end class UnsupportedPlatform < UnavailableError def initialize super "Unsupported platform #{RbConfig::CONFIG['host_os'].inspect}" end end attr_reader :options def initialize(opts = {}) options = opts.dup options.delete(:silent) @options = { title: "Notiffany" }. merge(self.class.const_get(:DEFAULTS)). merge(options).freeze @images_path = Pathname.new(__FILE__).dirname + "../../../images" _check_host_supported _require_gem _check_available(@options) end def title self.class.to_s[/.+::(\w+)$/, 1] end def name title.gsub(/([a-z])([A-Z])/, '\1_\2').downcase end def notify(message, opts = {}) new_opts = _notify_options(opts).freeze _perform_notify(message, new_opts) end def _image_path(image) images = [:failed, :pending, :success, :guard] images.include?(image) ? @images_path.join("#{image}.png").to_s : image end private # Override if necessary def _gem_name name end # Override if necessary def _supported_hosts :all end # Override def _check_available(_options) fail NotImplementedError end # Override def _perform_notify(_message, _opts) fail NotImplementedError end def _notification_type(image) [:failed, :pending, :success].include?(image) ? image : :notify end def _notify_options(overrides = {}) opts = @options.merge(overrides) img_type = opts.fetch(:image, :success) opts[:type] ||= _notification_type(img_type) opts[:image] = _image_path(img_type) opts end def _check_host_supported return if _supported_hosts == :all expr = /#{_supported_hosts * '|'}/ fail UnsupportedPlatform unless expr.match(RbConfig::CONFIG["host_os"]) end def _require_gem Kernel.require _gem_name unless _gem_name.nil? rescue LoadError, NameError fail RequireFailed, _gem_name end end end end notiffany-0.1.3/lib/notiffany/notifier/config.rb000066400000000000000000000013271352050573500216740ustar00rootroot00000000000000require "logger" module Notiffany class Notifier # Configuration class for Notifier class Config DEFAULTS = { notify: true }.freeze attr_reader :env_namespace attr_reader :logger attr_reader :notifiers def initialize(opts) options = DEFAULTS.merge(opts) @env_namespace = opts.fetch(:namespace, "notiffany") @logger = _setup_logger(options) @notify = options[:notify] @notifiers = opts.fetch(:notifiers, {}) end def notify? @notify end private def _setup_logger(opts) opts.fetch(:logger) do Logger.new($stderr).tap { |l| l.level = Logger::WARN } end end end end end notiffany-0.1.3/lib/notiffany/notifier/detected.rb000066400000000000000000000064241352050573500222130ustar00rootroot00000000000000require "nenv" require "yaml" require_relative "emacs" require_relative "file" require_relative "gntp" require_relative "growl" require_relative "libnotify" require_relative "notifysend" require_relative "rb_notifu" require_relative "terminal_notifier" require_relative "terminal_title" require_relative "tmux" module Notiffany class Notifier # @private api # TODO: use a socket instead of passing env variables to child processes # (currently probably only used by guard-cucumber anyway) YamlEnvStorage = Nenv::Builder.build do create_method(:notifiers=) { |data| YAML.dump(data || []) } create_method(:notifiers) { |data| data ? YAML.load(data) : [] } end # @private api class Detected NO_SUPPORTED_NOTIFIERS = "Notiffany could not detect any of the"\ " supported notification libraries." class NoneAvailableError < RuntimeError end class UnknownNotifier < RuntimeError def initialize(name) super @name = name end attr_reader :name def message "Unknown notifier: #{@name.inspect}" end end def initialize(supported, env_namespace, logger) @supported = supported @environment = YamlEnvStorage.new(env_namespace) @logger = logger end def reset @environment.notifiers = [] end def detect return unless _notifiers.empty? @supported.each do |group| group.detect do |name, _| begin _add(name, {}) true rescue Notifier::Base::UnavailableError => e @logger.debug "Notiffany: #{name} not available (#{e.message})." false end end end fail NoneAvailableError, NO_SUPPORTED_NOTIFIERS if _notifiers.empty? end def available @available ||= _notifiers.map do |entry| _to_module(entry[:name]).new(entry[:options]) end end # Called when user has notifier-specific config. # Honor the config by warning if something is wrong def add(name, opts) _add(name, opts) rescue Notifier::Base::UnavailableError => e @logger.warning("Notiffany: #{name} not available (#{e.message}).") end private def _add(name, opts) @available = nil all = _notifiers # Silently skip if it's already available, because otherwise # we'd have to do :turn_off, then configure, then :turn_on names = all.map(&:first).map(&:last) unless names.include?(name) fail UnknownNotifier, name unless (klass = _to_module(name)) klass.new(opts) # raises if unavailable @environment.notifiers = all << { name: name, options: opts } end # Just overwrite the options (without turning the notifier off or on), # so those options will be passed in next calls to notify() all.each { |item| item[:options] = opts if item[:name] == name } end def _to_module(name) @supported.each do |group| next unless (notifier = group.detect { |n, _| n == name }) return notifier.last end nil end def _notifiers @environment.notifiers end end end end notiffany-0.1.3/lib/notiffany/notifier/emacs.rb000066400000000000000000000062461352050573500215240ustar00rootroot00000000000000require 'notiffany/notifier/base' require 'shellany/sheller' require 'notiffany/notifier/emacs/client' module Notiffany class Notifier # Send a notification to Emacs with emacsclient # (http://www.emacswiki.org/emacs/EmacsClient). # class Emacs < Base DEFAULTS = { client: 'emacsclient', success: 'ForestGreen', failed: 'Firebrick', default: 'Black', fontcolor: 'White' }.freeze DEFAULT_ELISP_ERB = <" :foreground "<%= color %>") EOF private def _gem_name nil end def _check_available(options) return if Client.new(options.merge(elisp_erb: "'1'")).available? raise UnavailableError, 'Emacs client failed' end # Shows a system notification. # # @param [String] type the notification type. Either 'success', # 'pending', 'failed' or 'notify' # @param [String] title the notification title # @param [String] message the notification message body # @param [String] image the path to the notification image # @param [Hash] opts additional notification library options # @option opts [String] success the color to use for success # notifications (default is 'ForestGreen') # @option opts [String] failed the color to use for failure # notifications (default is 'Firebrick') # @option opts [String] pending the color to use for pending # notifications # @option opts [String] default the default color to use (default is # 'Black') # @option opts [String] client the client to use for notification # (default is 'emacsclient') # @option opts [String, Integer] priority specify an int or named key # (default is 0) # def _perform_notify(message, opts = {}) color = _emacs_color(opts[:type], opts) fontcolor = _emacs_color(:fontcolor, opts) opts = opts.merge(elisp_erb: _erb_for(opts[:elisp_file])) Client.new(opts).notify(fontcolor, color, message) end # Get the Emacs color for the notification type. # You can configure your own color by overwrite the defaults. # # @param [String] type the notification type # @param [Hash] options aditional notification options # # @option options [String] success the color to use for success # notifications (default is 'ForestGreen') # # @option options [String] failed the color to use for failure # notifications (default is 'Firebrick') # # @option options [String] pending the color to use for pending # notifications # # @option options [String] default the default color to use (default is # 'Black') # # @return [String] the name of the emacs color # def _emacs_color(type, options = {}) default = options.fetch(:default, DEFAULTS[:default]) options.fetch(type.to_sym, default) end def _erb_for(filename) return DEFAULT_ELISP_ERB unless filename IO.read(::File.expand_path(filename)) end end end end notiffany-0.1.3/lib/notiffany/notifier/emacs/000077500000000000000000000000001352050573500211675ustar00rootroot00000000000000notiffany-0.1.3/lib/notiffany/notifier/emacs/client.rb000066400000000000000000000025211352050573500227720ustar00rootroot00000000000000require 'erb' module Notiffany class Notifier class Emacs < Base # Handles evaluating ELISP code in Emacs via Erb class Client attr_reader :elisp_erb # Creates a safe binding with local variables for ERB class Elisp < ERB attr_reader :color attr_reader :bgcolor attr_reader :message def initialize(code, color, bgcolor, message) @color = color @bgcolor = bgcolor @message = message @code = code super(@code) end def result super(binding) end end def initialize(options) @client = options[:client] @elisp_erb = options[:elisp_erb] raise ArgumentError, 'No :elisp_erb option given!' unless elisp_erb end def available? script = Elisp.new(@elisp_erb, nil, nil, nil).result _emacs_eval({ 'ALTERNATE_EDITOR' => 'false' }, script) end def notify(color, bgcolor, message = nil) elisp = Elisp.new(elisp_erb, color, bgcolor, message).result _emacs_eval({ 'ALTERNATE_EDITOR' => 'false' }, elisp) end private def _emacs_eval(env, code) Shellany::Sheller.run(env, @client, '--eval', code) end end end end end notiffany-0.1.3/lib/notiffany/notifier/file.rb000066400000000000000000000026341352050573500213500ustar00rootroot00000000000000require "notiffany/notifier/base" module Notiffany class Notifier # Writes notifications to a file. # class File < Base DEFAULTS = { format: "%s\n%s\n%s\n" } private # @param [Hash] opts some options # @option opts [Boolean] path the path to a file where notification # message will be written # def _check_available(opts = {}) fail UnavailableError, "No :path option given" unless opts[:path] end # Writes the notification to a file. By default it writes type, title, # and message separated by newlines. # # @param [String] message the notification message body # @param [Hash] opts additional notification library options # @option opts [String] type the notification type. Either 'success', # 'pending', 'failed' or 'notify' # @option opts [String] title the notification title # @option opts [String] image the path to the notification image # @option opts [String] format printf style format for file contents # @option opts [String] path the path of where to write the file # def _perform_notify(message, opts = {}) fail UnavailableError, "No :path option given" unless opts[:path] str = format(opts[:format], opts[:type], opts[:title], message) ::File.write(opts[:path], str) end def _gem_name nil end end end end notiffany-0.1.3/lib/notiffany/notifier/gntp.rb000066400000000000000000000054001352050573500213730ustar00rootroot00000000000000require "notiffany/notifier/base" module Notiffany class Notifier # System notifications using the # [ruby_gntp](https://github.com/snaka/ruby_gntp) gem. # # This gem is available for OS X, Linux and Windows and sends system # notifications to the following system notification frameworks through the # # [Growl Network Transport # Protocol](http://www.growlforwindows.com/gfw/help/gntp.aspx): # # * [Growl](http://growl.info) # * [Growl for Windows](http://www.growlforwindows.com) # * [Growl for Linux](http://mattn.github.com/growl-for-linux) # * [Snarl](https://sites.google.com/site/snarlapp) class GNTP < Base DEFAULTS = { sticky: false } # Default options for the ruby gtnp client. CLIENT_DEFAULTS = { host: "127.0.0.1", password: "", port: 23_053 } def _supported_hosts %w(darwin linux linux-gnu freebsd openbsd sunos solaris mswin mingw cygwin) end def _gem_name "ruby_gntp" end def _check_available(_opts) end # Shows a system notification. # # @param [String] message the notification message body # @param [Hash] opts additional notification library options # @option opts [String] type the notification type. Either 'success', # 'pending', 'failed' or 'notify' # @option opts [String] title the notification title # @option opts [String] image the path to the notification image # @option opts [String] host the hostname or IP address to which to send # a remote notification # @option opts [String] password the password used for remote # notifications # @option opts [Integer] port the port to send a remote notification # @option opts [Boolean] sticky make the notification sticky # def _perform_notify(message, opts = {}) opts = { name: opts[:type].to_s, text: message, icon: opts[:image] }.merge(opts) _gntp_client(opts).notify(opts) end private def _gntp_client(opts = {}) @_client ||= begin gntp = ::GNTP.new( "Notiffany", opts.fetch(:host) { CLIENT_DEFAULTS[:host] }, opts.fetch(:password) { CLIENT_DEFAULTS[:password] }, opts.fetch(:port) { CLIENT_DEFAULTS[:port] } ) gntp.register( app_icon: _image_path(:guard), notifications: [ { name: "notify", enabled: true }, { name: "failed", enabled: true }, { name: "pending", enabled: true }, { name: "success", enabled: true } ] ) gntp end end end end end notiffany-0.1.3/lib/notiffany/notifier/growl.rb000066400000000000000000000056761352050573500215740ustar00rootroot00000000000000require "notiffany/notifier/base" module Notiffany class Notifier # System notifications using the # [growl](https://github.com/visionmedia/growl) gem. # # This gem is available for OS X and sends system notifications to # [Growl](http://growl.info) through the # [GrowlNotify](http://growl.info/downloads) executable. # # The `growlnotify` executable must be installed manually or by using # [Homebrew](http://mxcl.github.com/homebrew/). # # Sending notifications with this notifier will not show the different # notifications in the Growl preferences. Use the :gntp notifier if you # want to customize each notification type in Growl. # # @example Install `growlnotify` with Homebrew # brew install growlnotify # # @example Add the `growl` gem to your `Gemfile` # group :development # gem 'growl' # end # # @example Add the `:growl` notifier to your `Guardfile` # notification :growl # # @example Add the `:growl_notify` notifier with configuration options to # your `Guardfile` notification :growl, sticky: true, host: '192.168.1.5', # password: 'secret' # class Growl < Base INSTALL_GROWLNOTIFY = "Please install the 'growlnotify' executable'\ ' (available by installing the 'growl' gem)." # Default options for the growl notifications. DEFAULTS = { sticky: false, priority: 0 } def _supported_hosts %w(darwin) end def _check_available(_opts = {}) fail UnavailableError, INSTALL_GROWLNOTIFY unless ::Growl.installed? end # Shows a system notification. # # The documented options are for GrowlNotify 1.3, but the older options # are also supported. Please see `growlnotify --help`. # # Priority can be one of the following named keys: `Very Low`, # `Moderate`, `Normal`, `High`, `Emergency`. It can also be an integer # between -2 and 2. # # @param [String] message the notification message body # @param [Hash] opts additional notification library options # @option opts [String] type the notification type. Either 'success', # 'pending', 'failed' or 'notify' # @option opts [String] title the notification title # @option opts [String] image the path to the notification image # @option opts [Boolean] sticky make the notification sticky # @option opts [String, Integer] priority specify an int or named key # (default is 0) # @option opts [String] host the hostname or IP address to which to # send a remote notification # @option opts [String] password the password used for remote # notifications # def _perform_notify(message, opts = {}) opts = { name: "Notiffany" }.merge(opts) opts.select! { |k, _| ::Growl::Base.switches.include?(k) } ::Growl.notify(message, opts) end end end end notiffany-0.1.3/lib/notiffany/notifier/libnotify.rb000066400000000000000000000032071352050573500224250ustar00rootroot00000000000000require "notiffany/notifier/base" module Notiffany class Notifier # System notifications using the # [libnotify](https://github.com/splattael/libnotify) gem. # # This gem is available for Linux, FreeBSD, OpenBSD and Solaris and sends # system notifications to # Gnome [libnotify](http://developer.gnome.org/libnotify): # class Libnotify < Base DEFAULTS = { transient: false, append: true, timeout: 3 } private def _supported_hosts %w(linux linux-gnu freebsd openbsd sunos solaris) end def _check_available(_opts = {}) end # Shows a system notification. # # @param [String] message the notification message body # @param [Hash] opts additional notification library options # @option opts [String] type the notification type. Either 'success', # 'pending', 'failed' or 'notify' # @option opts [String] title the notification title # @option opts [String] image the path to the notification image # @option opts [Boolean] transient keep the notifications around after # display # @option opts [Boolean] append append onto existing notification # @option opts [Number, Boolean] timeout the number of seconds to display # (1.5 (s), 1000 (ms), false) # def _perform_notify(message, opts = {}) opts = opts.merge( summary: opts[:title], icon_path: opts[:image], body: message, urgency: opts[:urgency] || (opts[:type] == "failed" ? :normal : :low) ) ::Libnotify.show(opts) end end end end notiffany-0.1.3/lib/notiffany/notifier/notifysend.rb000066400000000000000000000056061352050573500226150ustar00rootroot00000000000000require "notiffany/notifier/base" require "shellany/sheller" module Notiffany class Notifier # System notifications using notify-send, a binary that ships with # the libnotify-bin package on many Debian-based distributions. # # @example Add the `:notifysend` notifier to your `Guardfile` # notification :notifysend # class NotifySend < Base # Default options for the notify-send notifications. DEFAULTS = { t: 3000, # Default timeout is 3000ms h: "int:transient:1" # Automatically close the notification } # Full list of options supported by notify-send. SUPPORTED = [:u, :t, :i, :c, :h] private # notify-send has no gem, just a binary to shell out def _gem_name nil end def _supported_hosts %w(linux linux-gnu freebsd openbsd sunos solaris) end def _check_available(_opts = {}) which = Shellany::Sheller.stdout("which notify-send") return true unless which.nil? || which.empty? fail UnavailableError, "libnotify-bin package is not installed" end # Shows a system notification. # # @param [String] message the notification message body # @param [Hash] opts additional notification library options # @option opts [String] type the notification type. Either 'success', # 'pending', 'failed' or 'notify' # @option opts [String] title the notification title # @option opts [String] image the path to the notification image # @option opts [String] c the notification category # @option opts [Number] t the number of milliseconds to display (1000, # 3000) # def _perform_notify(message, opts = {}) command = [opts[:title], message] opts = opts.merge( i: opts[:i] || opts[:image], u: opts[:u] || _notifysend_urgency(opts[:type]) ) Shellany::Sheller. run("notify-send", *_to_arguments(command, SUPPORTED, opts)) end # Converts Guards notification type to the best matching # notify-send urgency. # # @param [String] type the Guard notification type # @return [String] the notify-send urgency # def _notifysend_urgency(type) { failed: "normal", pending: "low" }.fetch(type, "low") end # Builds a shell command out of a command string and option hash. # # @param [String] command the command execute # @param [Array] supported list of supported option flags # @param [Hash] opts additional command options # # @return [Array] the command and its options converted to a # shell command. # def _to_arguments(command, supported, opts = {}) opts.inject(command) do |cmd, (flag, value)| supported.include?(flag) ? (cmd << "-#{flag}" << value.to_s) : cmd end end end end end notiffany-0.1.3/lib/notiffany/notifier/rb_notifu.rb000066400000000000000000000052551352050573500224220ustar00rootroot00000000000000require "notiffany/notifier/base" module Notiffany class Notifier # System notifications using the # [rb-notifu](https://github.com/stereobooster/rb-notifu) gem. # # This gem is available for Windows and sends system notifications to # [Notifu](http://www.paralint.com/projects/notifu/index.html): # # @example Add the `rb-notifu` gem to your `Gemfile` # group :development # gem 'rb-notifu' # end # class Notifu < Base # Default options for the rb-notifu notifications. DEFAULTS = { time: 3, icon: false, baloon: false, nosound: false, noquiet: false, xp: false } private def _supported_hosts %w(mswin mingw) end def _gem_name "rb-notifu" end def _check_available(_opts = {}) end # Shows a system notification. # # @param [String] message the notification message body # @param [Hash] opts additional notification library options # @option opts [String] type the notification type. Either 'success', # 'pending', 'failed' or 'notify' # @option opts [String] title the notification title # @option opts [String] image the path to the notification image # @option opts [Number] time the number of seconds to display (0 for # infinit) # @option opts [Boolean] icon specify an icon to use ("parent" uses the # icon of the parent process) # @option opts [Boolean] baloon enable ballon tips in the registry (for # this user only) # @option opts [Boolean] nosound do not play a sound when the tooltip is # displayed # @option opts [Boolean] noquiet show the tooltip even if the user is in # the quiet period that follows his very first login (Windows 7 and up) # @option opts [Boolean] xp use IUserNotification interface event when # IUserNotification2 is available # def _perform_notify(message, opts = {}) options = opts.dup options[:type] = _notifu_type(opts[:type]) options[:message] = message # The empty block is needed until # https://github.com/stereobooster/rb-notifu/pull/1 is merged ::Notifu.show(options) {} end # Converts generic notification type to the best matching # Notifu type. # # @param [String] type the generic notification type # @return [Symbol] the Notify notification type # def _notifu_type(type) case type.to_sym when :failed :error when :pending :warn else :info end end end end end notiffany-0.1.3/lib/notiffany/notifier/terminal_notifier.rb000066400000000000000000000034451352050573500241440ustar00rootroot00000000000000require "notiffany/notifier/base" module Notiffany class Notifier # System notifications using the # # [terminal-notifier](https://github.com/Springest/terminal-notifier-guard) # # gem. # # This gem is available for OS X 10.8 Mountain Lion and sends notifications # to the OS X notification center. class TerminalNotifier < Base DEFAULTS = { app_name: "Notiffany" } ERROR_ONLY_OSX10 = "The :terminal_notifier only runs"\ " on Mac OS X 10.8 and later." def _supported_hosts %w(darwin) end def _gem_name "terminal-notifier-guard" end def _check_available(_opts = {}) return if ::TerminalNotifier::Guard.available? fail UnavailableError, ERROR_ONLY_OSX10 end # Shows a system notification. # # @param [String] message the notification message body # @param [Hash] opts additional notification library options # @option opts [String] type the notification type. Either 'success', # 'pending', 'failed' or 'notify' # @option opts [String] title the notification title # @option opts [String] image the path to the notification image (ignored) # @option opts [String] app_name name of your app # @option opts [String] execute a command # @option opts [String] activate an app bundle # @option opts [String] open some url or file # def _perform_notify(message, opts = {}) title = [opts[:app_name], opts[:type].downcase.capitalize].join(" ") opts = { title: title }.merge(opts) opts[:message] = message opts[:title] ||= title opts.delete(:image) opts.delete(:app_name) ::TerminalNotifier::Guard.execute(false, opts) end end end end notiffany-0.1.3/lib/notiffany/notifier/terminal_title.rb000066400000000000000000000016651352050573500234500ustar00rootroot00000000000000require "notiffany/notifier/base" module Notiffany class Notifier # Shows system notifications in the terminal title bar. # class TerminalTitle < Base DEFAULTS = {} # Clears the terminal title def turn_off STDOUT.puts "\e]2;\a" end private def _gem_name nil end def _check_available(_options) end # Shows a system notification. # # @param [Hash] opts additional notification library options # @option opts [String] message the notification message body # @option opts [String] type the notification type. Either 'success', # 'pending', 'failed' or 'notify' # @option opts [String] title the notification title # def _perform_notify(message, opts = {}) first_line = message.sub(/^\n/, "").sub(/\n.*/m, "") STDOUT.puts "\e]2;[#{opts[:title]}] #{first_line}\a" end end end end notiffany-0.1.3/lib/notiffany/notifier/tmux.rb000066400000000000000000000103401352050573500214170ustar00rootroot00000000000000require "notiffany/notifier/base" require "notiffany/notifier/tmux/client" require "notiffany/notifier/tmux/session" require "notiffany/notifier/tmux/notification" # TODO: this probably deserves a gem of it's own module Notiffany class Notifier # Changes the color of the Tmux status bar and optionally # shows messages in the status bar. class Tmux < Base @session = nil DEFAULTS = { tmux_environment: "TMUX", success: "green", failed: "red", pending: "yellow", default: "green", timeout: 5, display_message: false, default_message_format: "%s - %s", default_message_color: "white", display_on_all_clients: false, display_title: false, default_title_format: "%s - %s", line_separator: " - ", change_color: true, color_location: "status-left-bg" } class Error < RuntimeError end ERROR_NOT_INSIDE_TMUX = ":tmux notifier is only available inside a "\ "TMux session." ERROR_ANCIENT_TMUX = "Your tmux version is way too old (%s)!" # Notification starting, save the current Tmux settings # and quiet the Tmux output. # def turn_on self.class._start_session end # Notification stopping. Restore the previous Tmux state # if available (existing options are restored, new options # are unset) and unquiet the Tmux output. # def turn_off self.class._end_session end private def _gem_name nil end def _check_available(opts = {}) @session ||= nil # to avoid unitialized error fail "PREVIOUS TMUX SESSION NOT CLEARED!" if @session var_name = opts[:tmux_environment] fail Error, ERROR_NOT_INSIDE_TMUX unless ENV.key?(var_name) version = Client.version fail Error, format(ERROR_ANCIENT_TMUX, version) if version < 1.7 true rescue Error => e fail UnavailableError, e.message end # Shows a system notification. # # By default, the Tmux notifier only makes # use of a color based notification, changing the background color of the # `color_location` to the color defined in either the `success`, # `failed`, `pending` or `default`, depending on the notification type. # # You may enable an extra explicit message by setting `display_message` # to true, and may further disable the colorization by setting # `change_color` to false. # # @param [String] message the notification message # @param [Hash] options additional notification library options # @option options [String] type the notification type. Either 'success', # 'pending', 'failed' or 'notify' # @option options [String] message the notification message body # @option options [String] image the path to the notification image # @option options [Boolean] change_color whether to show a color # notification # @option options [String,Array] color_location the location where to draw # the color notification # @option options [Boolean] display_message whether to display a message # or not # @option options [Boolean] display_on_all_clients whether to display a # message on all tmux clients or not # def _perform_notify(message, options = {}) locations = Array(options[:color_location]) type = options[:type].to_s title = options[:title] tmux = Notification.new(type, options) tmux.colorize(locations) if options[:change_color] tmux.display_title(title, message) if options[:display_title] tmux.display_message(title, message) if options[:display_message] end class << self def _start_session fail "Already turned on!" if @session @session = Session.new end def _end_session fail "Already turned off!" unless @session @session.close @session = nil end def _session @session end end end end end notiffany-0.1.3/lib/notiffany/notifier/tmux/000077500000000000000000000000001352050573500210745ustar00rootroot00000000000000notiffany-0.1.3/lib/notiffany/notifier/tmux/client.rb000066400000000000000000000051501352050573500227000ustar00rootroot00000000000000require "shellany/sheller" module Notiffany class Notifier class Tmux < Base # Class for actually calling TMux to run commands class Client CLIENT = "tmux".freeze class << self def version begin Float(_capture("-V")[/\d+\.\d+/]) rescue NoMethodError, TypeError raise Base::UnavailableError, "Could not find tmux" end end def _capture(*args) Shellany::Sheller.stdout(([CLIENT] + args).join(" ")) end def _run(*args) Shellany::Sheller.run(([CLIENT] + args).join(" ")) end end def initialize(client) @client = client end def clients return [@client] unless @client == :all ttys = _capture("list-clients", "-F", "'\#{client_tty}'") ttys = ttys.split(/\n/) # if user is running 'tmux -C' remove this client from list ttys.delete("(null)") ttys end def set(key, value) clients.each do |client| args = client ? ["-t", client.strip] : nil _run("set", "-q", *args, key, value) end end def display_message(message) clients.each do |client| args = ["-c", client.strip] if client # TODO: should properly escape message here _run("display", *args, "'#{message}'") end end def unset(key, value) clients.each do |client| _run(*_all_args_for(key, value, client)) end end def parse_options output = _capture("show", "-t", @client) Hash[output.lines.map { |line| _parse_option(line) }] end def message_fg=(color) set("message-fg", color) end def message_bg=(color) set("message-bg", color) end def display_time=(time) set("display-time", time) end def title=(string) # TODO: properly escape? set("set-titles-string", "'#{string}'") end private def _run(*args) self.class._run(*args) end def _capture(*args) self.class._capture(*args) end def _parse_option(line) line.partition(" ").map(&:strip).reject(&:empty?) end def _all_args_for(key, value, client) unset = value ? [] : %w(-u) args = client ? ["-t", client.strip] : [] ["set", "-q", *unset, *args, key, *[value].compact] end end end end end notiffany-0.1.3/lib/notiffany/notifier/tmux/notification.rb000066400000000000000000000034151352050573500241120ustar00rootroot00000000000000module Notiffany class Notifier class Tmux < Base # Wraps a notification with it's options class Notification def initialize(type, options) @type = type @options = options @color = options[type.to_sym] || options[:default] @separator = options[:line_separator] @message_color = _value_for(:message_color) @client = Client.new(options[:display_on_all_clients] ? :all : nil) end def display_title(title, message) title_format = _value_for(:title_format) teaser_message = message.split("\n").first display_title = format(title_format, title, teaser_message) client.title = display_title end def display_message(title, message) message = _message_for(title, message) client.display_time = options[:timeout] * 1000 client.message_fg = message_color client.message_bg = color client.display_message(message) end def colorize(locations) locations.each do |location| client.set(location, color) end end private attr_reader :type attr_reader :options attr_reader :color attr_reader :message_color attr_reader :client attr_reader :separator def _value_for(field) format = "#{type}_#{field}".to_sym default = options["default_#{field}".to_sym] options.fetch(format, default) end def _message_for(title, message) message_format = _value_for(:message_format) formatted_message = message.split("\n").join(separator) format(message_format, title, formatted_message) end end end end end notiffany-0.1.3/lib/notiffany/notifier/tmux/session.rb000066400000000000000000000024101352050573500231010ustar00rootroot00000000000000module Notiffany class Notifier class Tmux < Base # Preserves TMux settings for all tmux sessions class Session def initialize @options_store = {} # NOTE: we are reading the settings of all clients # - regardless of the :display_on_all_clients option # Ideally, this should be done incrementally (e.g. if we start with # "current" client and then override the :display_on_all_clients to # true, only then the option store should be updated to contain # settings of all clients Client.new(:all).clients.each do |client| @options_store[client] = { "status-left-bg" => nil, "status-right-bg" => nil, "status-left-fg" => nil, "status-right-fg" => nil, "message-bg" => nil, "message-fg" => nil, "display-time" => nil }.merge(Client.new(client).parse_options) end end def close @options_store.each do |client, options| options.each do |key, value| Client.new(client).unset(key, value) end end @options_store = nil end end end end end notiffany-0.1.3/lib/notiffany/version.rb000066400000000000000000000000511352050573500202660ustar00rootroot00000000000000module Notiffany VERSION = "0.1.3" end notiffany-0.1.3/notiffany.gemspec000066400000000000000000000024271352050573500170640ustar00rootroot00000000000000# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'notiffany/version' Gem::Specification.new do |spec| spec.name = "notiffany" spec.version = Notiffany::VERSION spec.authors = [ "Cezary Baginski", "Rémy Coutable", "Thibaud Guillaume-Gentil" ] spec.email = ["cezary@chronomantic.net"] spec.summary = 'Notifier library (extracted from Guard project)' spec.description = <<-EOF Wrapper libray for most popular notification libraries such as Growl, Libnotify, Notifu EOF spec.homepage = "https://github.com/guard/notiffany" spec.license = "MIT" git_files = `git ls-files -z`.split("\x0") files = git_files.select { |f| %r{^lib/.*$} =~ f } files += %w(README.md LICENSE.txt) # skip the large images/guard.png files += %w(images/pending.png images/failed.png images/success.png) spec.files = files spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] spec.add_runtime_dependency "nenv", "~> 0.1" spec.add_runtime_dependency "shellany", "~> 0.0" spec.add_development_dependency "bundler", "~> 1.7" end notiffany-0.1.3/spec/000077500000000000000000000000001352050573500144475ustar00rootroot00000000000000notiffany-0.1.3/spec/lib/000077500000000000000000000000001352050573500152155ustar00rootroot00000000000000notiffany-0.1.3/spec/lib/notiffany/000077500000000000000000000000001352050573500172125ustar00rootroot00000000000000notiffany-0.1.3/spec/lib/notiffany/notifier/000077500000000000000000000000001352050573500210315ustar00rootroot00000000000000notiffany-0.1.3/spec/lib/notiffany/notifier/base_spec.rb000066400000000000000000000105061352050573500233040ustar00rootroot00000000000000require "notiffany/notifier/base" # TODO: no point in testing the base class, really module Notiffany RSpec.describe Notifier::Base do let(:fake) { double "fake_lib" } let(:options) { {} } let(:os) { "solaris" } subject { Notifier::FooBar.new({ fake: fake }.merge(options)) } before do allow(Kernel).to receive(:require) allow(RbConfig::CONFIG).to receive(:[]).with("host_os") { os } end class Notifier class FooBar < Notifier::Base DEFAULTS = { foo: :bar } private def _supported_hosts %w(freebsd solaris) end def _perform_notify(message, options) options[:fake].notify(message, options) end def _check_available(_options) true end end end describe "#initialize" do context "on unsupported os" do let(:os) { "mswin" } it "fails" do expect { subject }.to raise_error(Notifier::Base::UnsupportedPlatform) end end context "on supported os" do let(:os) { "freebsd" } context "library loads normally" do it "returns true" do expect(Kernel).to receive(:require).with("foo_bar") expect { subject }.to_not raise_error end end context "when library fails to load" do before do allow(Kernel).to receive(:require).with("foo_bar"). and_raise LoadError end it "fails with error" do expect { subject }. to raise_error(Notifier::Base::RequireFailed) end end end end describe "#name" do it 'un-modulizes the class, replaces "xY" with "x_Y" and downcase' do expect(subject.name).to eq "foo_bar" end end describe "#title" do it "un-modulize the class" do expect(subject.title).to eq "FooBar" end end describe "#notify" do let(:opts) { {} } context "with no notify title overrides" do it "supplies default title" do expect(fake).to receive(:notify). with("foo", hash_including(title: "Notiffany")) subject.notify("foo", opts) end end context "with notify title override" do let(:opts) { { title: "Hi" } } it "uses given title" do expect(fake).to receive(:notify). with("foo", hash_including(title: "Hi")) subject.notify("foo", opts) end end context "with no type overrides" do it "supplies default type" do expect(fake).to receive(:notify). with("foo", hash_including(type: :success)) subject.notify("foo", opts) end end context "with type given" do let(:opts) { { type: :foo } } it "uses given type" do expect(fake).to receive(:notify). with("foo", hash_including(type: :foo)) subject.notify("foo", opts) end end context "with no image overrides" do it "supplies default image" do expect(fake).to receive(:notify). with("foo", hash_including(image: /success.png$/)) subject.notify("foo", opts) end end %w(failed pending success guard).each do |img| context "with #{img.to_sym.inspect} image" do let(:opts) { { image: img.to_sym } } it "converts to image path" do expect(fake).to receive(:notify). with("foo", hash_including(image: /#{img}.png$/)) subject.notify("foo", opts) end end end context "with a custom image" do let(:opts) { { image: "foo.jpg" } } it "uses given image" do expect(fake).to receive(:notify). with("foo", hash_including(image: "foo.jpg")) subject.notify("foo", opts) end end context "with nil image" do let(:opts) { { image: nil } } it "set the notify image to nil" do expect(fake).to receive(:notify). with("foo", hash_including(image: nil)) subject.notify("foo", opts) end it "uses the default type" do expect(fake).to receive(:notify). with("foo", hash_including(type: :notify)) subject.notify("foo", opts) end end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/detected_spec.rb000066400000000000000000000121751352050573500241570ustar00rootroot00000000000000require "notiffany/notifier/detected" module Notiffany class Notifier RSpec.describe YamlEnvStorage do let(:subject) { YamlEnvStorage.new("notiffany_tests_foo") } describe "#notifiers" do context "when set to empty array" do before { subject.notifiers = [] } specify { expect(subject.notifiers).to be_empty } end context "when set to nil" do before { subject.notifiers = nil } specify { expect(subject.notifiers).to be_empty } end context "when env is empty" do before { ENV['NOTIFFANY_TESTS_FOO_NOTIFIERS'] = nil } specify { expect(subject.notifiers).to be_empty } end end end RSpec.describe(Detected, exclude_stubs: [YamlEnvStorage]) do let(:logger) { double("Logger", debug: nil) } subject { described_class.new(supported, "notiffany_tests", logger) } let(:env) { instance_double(YamlEnvStorage) } let(:foo_mod) { double("foo_mod") } let(:bar_mod) { double("bar_mod") } let(:baz_mod) { double("baz_mod") } let(:foo_obj) { double("foo_obj") } let(:baz_obj) { double("baz_obj") } let(:supported) { [foo: foo_mod, baz: baz_mod] } before do allow(YamlEnvStorage).to receive(:new).and_return(env) allow(env).to receive(:notifiers) do fail "stub me: notifiers" end allow(env).to receive(:notifiers=) do |args| fail "stub me: notifiers=(#{args.inspect})" end end describe "#available" do context "with detected notifiers" do let(:available) do [ { name: :foo, options: {} }, { name: :baz, options: { opt1: 3 } } ] end let(:expected) { [foo_obj, baz_obj] } before do allow(foo_mod).to receive(:new).and_return(foo_obj) allow(baz_mod).to receive(:new).and_return(baz_obj) allow(env).to receive(:notifiers).and_return(available) end it "returns hash with detected notifier options" do expect(subject.available).to eq(expected) end end end describe "#add" do before do allow(env).to receive(:notifiers).and_return(notifiers) end context "with no detected notifiers" do let(:notifiers) { [] } context "when unknown" do it "does not add the library" do expect(env).to_not receive(:notifiers=) expect { subject.add(:unknown, {}) }. to raise_error(Notifier::Detected::UnknownNotifier) end end end context "with manually configured notifiers" do let(:notifiers) { [] } context "when not available" do before do allow(foo_mod).to receive(:new). with(foo: :bar). and_raise(Notifier::Base::UnavailableError, "something failed") allow(logger).to receive(:warning) end it "does not add the library" do expect(env).to_not receive(:notifiers=) subject.add(:foo, foo: :bar) end it "does not raise an error" do expect { subject.add(:foo, foo: :bar) }.to_not raise_error end it "shows a warning" do expect(logger).to receive(:warning). with("Notiffany: foo not available (something failed).") subject.add(:foo, foo: :bar) end end end end describe "#detect" do context "with some detected notifiers" do before do allow(env).to receive(:notifiers).and_return([]) allow(foo_mod).to receive(:new).and_return(foo_obj) allow(baz_mod).to receive(:new). and_raise(Notifier::Base::UnavailableError, "some failure") end let(:detected) { [{ name: :foo, options: {} }] } it "add detected notifiers to available" do expect(env).to receive(:notifiers=) do |args| expect(args).to eq(detected) end allow(env).to receive(:notifiers).and_return([], [], detected) subject.detect end end context "without any detected notifiers" do before do allow(env).to receive(:notifiers).and_return([]) allow(foo_mod).to receive(:new). and_raise(Notifier::Base::UnavailableError, "some error") allow(baz_mod).to receive(:new). and_raise(Notifier::Base::UnavailableError, "some error") end let(:error) { described_class::NoneAvailableError } let(:msg) { /could not detect any of the supported notification/ } it { expect { subject.detect }.to raise_error(error, msg) } end end describe "#reset" do before do allow(env).to receive(:notifiers=) end it "resets the detected notifiers" do expect(env).to receive(:notifiers=).with([]) subject.reset end end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/emacs/000077500000000000000000000000001352050573500221215ustar00rootroot00000000000000notiffany-0.1.3/spec/lib/notiffany/notifier/emacs/client_spec.rb000066400000000000000000000046401352050573500247420ustar00rootroot00000000000000require 'notiffany/notifier/emacs' require 'notiffany/notifier/emacs/client' RSpec.describe Notiffany::Notifier::Emacs::Client do let(:sheller) { Shellany::Sheller } let(:default_options) { { client: 'emacsclient', elisp_erb: elisp_erb } } let(:options) { default_options } subject { described_class.new(options) } before do allow(sheller).to receive(:run) do |*args| raise "stub me: #{sheller.class}(#{args.map(&:inspect) * ','})" end end describe '#initialize' do context 'when constructed without elisp_erb' do let(:elisp_erb) { nil } let(:options) { default_options.merge(elisp_erb: nil) } it 'fails with an error' do expect do subject end.to raise_error(ArgumentError, 'No :elisp_erb option given!') end end end describe '#available?' do let(:elisp_erb) { "'<%= 0+1 %>'" } before do allow(sheller).to receive(:run).with( { 'ALTERNATE_EDITOR' => 'false' }, 'emacsclient', '--eval', "'1'" ).and_return(result) end context 'with a working client command' do let(:result) { true } it { is_expected.to be_available } end context 'when the client commmand does not exist' do let(:result) { nil } it { is_expected.to_not be_available } end context 'when the client command fails' do let(:result) { false } it { is_expected.to_not be_available } end end # TODO: handle failure emacs failure due to elisp error? describe '#notify' do context 'when constructed with valid elisp Erb' do let(:elisp_erb) do "( print 'color is <%= color %>, bg color is <%= bgcolor %>' )" end let(:result) { true } it 'evaluates using given colors' do expect(sheller).to receive(:run).with( anything, 'emacsclient', '--eval', "( print 'color is Green, bg color is Black' )" ).and_return(result) subject.notify('Green', 'Black') end context 'with a message' do let(:elisp_erb) { "( print 'Info: <%= message %>' )" } it 'evaluates using given message' do expect(sheller).to receive(:run).with( anything, 'emacsclient', '--eval', "( print 'Info: FooBar' )" ).and_return(result) subject.notify('Green', 'Black', 'FooBar') end end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/emacs_spec.rb000066400000000000000000000105461352050573500234660ustar00rootroot00000000000000require 'notiffany/notifier/emacs' RSpec.describe Notiffany::Notifier::Emacs do let(:options) { {} } subject { described_class.new(options) } let(:availability_command_result) { true } let(:availability_checking_client) do instance_double( described_class::Client, available?: availability_command_result ) end before do allow(described_class::Client).to receive(:new) do |*args| args_info = args.map(&:inspect) * ',' raise "stub me: #{described_class::Client}.new(#{args_info})" end allow(described_class::Client).to receive(:new) .with(hash_including(elisp_erb: "'1'")) .and_return(availability_checking_client) end before do allow(File).to receive(:expand_path) do |*args| raise "stub me: File.expand_path(#{args.map(&:inspect) * ','})" end allow(IO).to receive(:read) do |*args| raise "stub me: IO.read(#{args.map(&:inspect) * ','})" end end describe '#initialize' do context 'when the client command works' do let(:availability_command_result) { true } it 'works' do subject end end context 'when the client command fails' do let(:availability_command_result) { false } it 'fails' do expect { subject } .to raise_error(Notiffany::Notifier::Base::UnavailableError) end end end describe '#notify' do let(:notifying_client) { instance_double(described_class::Client) } before do allow(notifying_client).to receive(:notify) default_elisp = { elisp_erb: described_class::DEFAULT_ELISP_ERB } allow(described_class::Client).to receive(:new) .with(hash_including(default_elisp)) .and_return(notifying_client) end describe 'color' do context 'when left default' do context 'without overriding global options' do it 'is set to default' do expect(notifying_client).to receive(:notify) .with('White', 'ForestGreen', anything) subject.notify('any message') end end end context 'when set globally' do let(:options) { { success: 'Pink', silent: true } } context 'when no overring notification options' do it 'is set to global value' do expect(notifying_client).to receive(:notify) .with('White', 'Pink', anything) subject.notify('any message') end end end context 'when set during notification' do describe 'for :success' do let(:notification_options) { { success: 'Orange' } } it 'is set from the notification value' do expect(notifying_client).to receive(:notify) .with('White', 'Orange', anything) subject.notify('any message', notification_options) end end describe 'for :pending' do let(:notification_options) { { type: :pending, pending: 'Yellow' } } it 'is set from the notification value' do expect(notifying_client).to receive(:notify) .with('White', 'Yellow', anything) subject.notify('any message', notification_options) end end end end context 'with no elisp file' do let(:options) { {} } it 'uses default elisp notification code' do expected_elisp_erb = <" :foreground "<%= color %>") EOF expected = { elisp_erb: expected_elisp_erb } expect(described_class::Client).to receive(:new) .with(hash_including(expected)) .and_return(notifying_client) subject.notify('any message') end end context 'with elisp file' do let(:options) { { elisp_file: '~/.my_elisp_script' } } before do allow(File).to receive(:expand_path) .with('~/.my_elisp_script') .and_return('/foo/bar') allow(IO).to receive(:read) .with('/foo/bar') .and_return('( print "hello, color is: <%= color %>" )') end it 'passes evaluated erb to client' do expected = { elisp_erb: '( print "hello, color is: <%= color %>" )' } expect(described_class::Client).to receive(:new) .with(hash_including(expected)) .and_return(notifying_client) subject.notify('any message') end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/file_spec.rb000066400000000000000000000044451352050573500233160ustar00rootroot00000000000000require "notiffany/notifier/file" module Notiffany RSpec.describe Notifier::File do let(:options) { {} } subject { described_class.new(options) } describe "#available" do context "with path option" do let(:options) { { path: ".guard_result" } } it "works" do subject end end context "with no path option" do it "fails" do expect { subject }.to raise_error(Notifier::Base::UnavailableError) end end end describe "#notify" do let(:options) { { path: "/tmp/foo" } } context "with options passed at initialization" do let(:options) { { path: "tmp/guard_result", silent: true } } it "uses these options by default" do expect(File).to receive(:write). with("tmp/guard_result", "success\nany title\nany message\n") subject.notify("any message", title: "any title") end it "overwrites object options with passed options" do expect(File).to receive(:write). with("tmp/guard_result_final", "success\nany title\nany message\n") subject.notify("any message", title: "any title", path: "tmp/guard_result_final") end end it "writes to a file on success" do expect(File).to receive(:write). with("tmp/guard_result", "success\nany title\nany message\n") subject.notify("any message", title: "any title", path: "tmp/guard_result") end it "also writes to a file on failure" do expect(File).to receive(:write). with("tmp/guard_result", "failed\nany title\nany message\n") subject.notify("any message", type: :failed, title: "any title", path: "tmp/guard_result") end # We don't have a way to return false in .available? when no path is # specified. So, we just don't do anything in .notify if there's no path. it "does not write to a file if no path is specified" do expect(File).to_not receive(:write) expect { subject.notify("any message", path: nil) }. to raise_error(Notifier::Base::UnavailableError) end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/gntp_spec.rb000066400000000000000000000075431352050573500233510ustar00rootroot00000000000000require "notiffany/notifier/gntp" module Notiffany RSpec.describe Notifier::GNTP do let(:gntp) { double("GNTP").as_null_object } let(:options) { {} } let(:os) { "solaris" } subject { described_class.new(options) } before do stub_const "GNTP", gntp allow(Kernel).to receive(:require) allow(RbConfig::CONFIG).to receive(:[]).with("host_os") { os } end describe ".available?" do context "host is not supported" do let(:os) { "foobar" } it "fails" do expect { subject }.to raise_error(Notifier::Base::UnsupportedPlatform) end end context "host is supported" do let(:os) { "darwin" } it "requires ruby_gntp" do expect(Kernel).to receive(:require).with("ruby_gntp") subject end end end describe "#client" do before do allow(::GNTP).to receive(:new) { gntp } allow(gntp).to receive(:register) end it "creates a new GNTP client and memoize it" do expect(::GNTP).to receive(:new). with("Notiffany", "127.0.0.1", "", 23_053).once { gntp } subject.notify("Welcome") subject.notify("Welcome") end it "calls #register on the client and memoize it" do expect(::GNTP).to receive(:new). with("Notiffany", "127.0.0.1", "", 23_053).once { gntp } expect(gntp).to receive(:register).once subject.notify("Welcome") subject.notify("Welcome") end end describe "#notify" do before do expect(::GNTP).to receive(:new).and_return(gntp) end context "with options passed at initialization" do let(:options) { { title: "Hello", silent: true } } it "uses these options by default" do expect(gntp).to receive(:notify).with( hash_including( sticky: false, name: "success", title: "Hello", text: "Welcome", icon: "/tmp/welcome.png" ) ) subject.notify( "Welcome", type: :success, image: "/tmp/welcome.png" ) end it "overwrites object options with passed options" do expect(gntp).to receive(:notify).with( hash_including( sticky: false, name: "success", title: "Welcome", text: "Welcome to Guard", icon: "/tmp/welcome.png" ) ) subject.notify( "Welcome to Guard", type: :success, title: "Welcome", image: "/tmp/welcome.png" ) end end context "without additional options" do it "shows the notification with the default options" do expect(gntp).to receive(:notify).with( hash_including( sticky: false, name: "success", title: "Welcome", text: "Welcome to Guard", icon: "/tmp/welcome.png" ) ) subject.notify( "Welcome to Guard", type: :success, title: "Welcome", image: "/tmp/welcome.png" ) end end context "with additional options" do it "can override the default options" do expect(gntp).to receive(:notify).with( hash_including( sticky: true, name: "pending", title: "Waiting", text: "Waiting for something", icon: "/tmp/wait.png" ) ) subject.notify( "Waiting for something", type: :pending, title: "Waiting", image: "/tmp/wait.png", sticky: true ) end end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/growl_spec.rb000066400000000000000000000103011352050573500235150ustar00rootroot00000000000000require "notiffany/notifier/growl" module Notiffany RSpec.describe Notifier::Growl do module FakeGrowl def self.notify(_message, _opts) end def self.installed? end class Base def self.switches [:sticky, :priority, :name, :title, :image] end end end let(:growl) { FakeGrowl } let(:options) { {} } let(:os) { "solaris" } subject { described_class.new(options) } before do allow(Kernel).to receive(:require) allow(RbConfig::CONFIG).to receive(:[]).with("host_os") { os } stub_const "Growl", growl allow(growl).to receive(:installed?).and_return(true) allow(growl).to receive(:notify) end describe "#initialize" do context "host is not supported" do let(:os) { "mswin" } it "fails" do expect { subject }.to raise_error(Notifier::Base::UnavailableError) end end context "host is supported" do let(:os) { "darwin" } it "works" do subject end context "when Growl is not installed" do before do allow(growl).to receive(:installed?).and_return(false) end it "fails" do expect { subject }.to raise_error(Notifier::Base::UnavailableError) end end end end describe "#notify" do let(:os) { "darwin" } context "with options passed at initialization" do let(:options) { { title: "Hello", silent: true } } it "uses these options by default" do expect(growl).to receive(:notify).with( "Welcome!", hash_including( sticky: false, priority: 0, name: "Notiffany", title: "Hello", image: "/tmp/welcome.png" ) ) subject.notify("Welcome!", image: "/tmp/welcome.png") end it "overwrites object options with passed options" do expect(growl).to receive(:notify).with( "Welcome!", hash_including( sticky: false, priority: 0, name: "Notiffany", title: "Welcome", image: "/tmp/welcome.png" ) ) subject.notify("Welcome!", title: "Welcome", image: "/tmp/welcome.png") end end context "without additional options" do it "shows the notification with the default options" do expect(growl).to receive(:notify).with( "Welcome!", hash_including( sticky: false, priority: 0, name: "Notiffany", title: "Welcome", image: "/tmp/welcome.png" ) ) subject.notify( "Welcome!", title: "Welcome", image: "/tmp/welcome.png") end end context "with additional options" do it "can override the default options" do expect(growl).to receive(:notify).with( "Waiting for something", hash_including( sticky: true, priority: 2, name: "Notiffany", title: "Waiting", image: "/tmp/wait.png" ) ) subject.notify( "Waiting for something", type: :pending, title: "Waiting", image: "/tmp/wait.png", sticky: true, priority: 2 ) end end context "with options Growl cannot handle" do it "passes options only Growl can handle" do expect(growl).to receive(:notify).with( "Foo", sticky: true, priority: 2, name: "Notiffany", title: "Waiting", image: "/tmp/wait.png" ) subject.notify( "Foo", foo: :bar, type: :pending, title: "Waiting", image: "/tmp/wait.png", sticky: true, priority: 2 ) end end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/libnotify_spec.rb000066400000000000000000000070021352050573500243660ustar00rootroot00000000000000require "notiffany/notifier/libnotify" module Notiffany RSpec.describe Notifier::Libnotify do let(:options) { {} } let(:os) { "solaris" } subject { described_class.new(options) } before do stub_const "Libnotify", double allow(Kernel).to receive(:require) allow(RbConfig::CONFIG).to receive(:[]).with("host_os") { os } end describe "#initialize" do let(:os) { "mswin" } context "with unsupported host" do it "does not require libnotify" do expect(Kernel).to_not receive(:require) expect { subject }.to raise_error(Notifier::Base::UnsupportedPlatform) end end context "host is supported" do let(:os) { "linux" } it "requires libnotify" do expect(Kernel).to receive(:require).and_return(true) subject end end end describe "#notify" do context "with options passed at initialization" do let(:options) { { title: "Hello", silent: true } } it "uses these options by default" do expect(::Libnotify).to receive(:show).with( hash_including( transient: false, append: true, timeout: 3, urgency: :low, summary: "Hello", body: "Welcome to Guard", icon_path: "/tmp/welcome.png" ) ) subject.notify("Welcome to Guard", image: "/tmp/welcome.png") end it "overwrites object options with passed options" do expect(::Libnotify).to receive(:show).with( hash_including( transient: false, append: true, timeout: 3, urgency: :low, summary: "Welcome", body: "Welcome to Guard", icon_path: "/tmp/welcome.png" ) ) subject.notify("Welcome to Guard", title: "Welcome", image: "/tmp/welcome.png") end end context "without additional options" do it "shows the notification with the default options" do expect(::Libnotify).to receive(:show).with( hash_including( transient: false, append: true, timeout: 3, urgency: :low, summary: "Welcome", body: "Welcome to Guard", icon_path: "/tmp/welcome.png" ) ) subject.notify("Welcome to Guard", title: "Welcome", image: "/tmp/welcome.png") end end context "with additional options" do it "can override the default options" do expect(::Libnotify).to receive(:show).with( hash_including( transient: true, append: false, timeout: 5, urgency: :critical, summary: "Waiting", body: "Waiting for something", icon_path: "/tmp/wait.png" ) ) subject.notify("Waiting for something", type: :pending, title: "Waiting", image: "/tmp/wait.png", transient: true, append: false, timeout: 5, urgency: :critical ) end end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/notifysend_spec.rb000066400000000000000000000121121352050573500245470ustar00rootroot00000000000000require "notiffany/notifier/notifysend" module Notiffany class Notifier RSpec.describe NotifySend do let(:options) { {} } let(:os) { "solaris" } subject { described_class.new(options) } let(:sheller) { class_double("Shellany::Sheller") } before do allow(Kernel).to receive(:require) allow(RbConfig::CONFIG).to receive(:[]).with("host_os") { os } stub_const "NotifySend", double stub_const("Shellany::Sheller", sheller) allow(sheller).to receive(:stdout) end describe "#initialize" do context "host is not supported" do let(:os) { "mswin" } it "do not check if the binary is available" do expect(sheller).to_not receive(:stdout) expect { subject }.to raise_error(Base::UnsupportedPlatform) end end context "host is supported" do let(:os) { "linux" } describe 'check_available' do it "checks if the binary is available" do expect(sheller).to receive(:stdout).with("which notify-send"). and_return("foo\n") subject end context 'binary check returns nil' do before do allow(sheller).to receive(:stdout).with("which notify-send"). and_return(nil) end it 'should raise an UnavailableError' do expect { subject }.to raise_error(Base::UnavailableError) end end end end end describe "#notify" do before do allow(sheller).to receive(:stdout).with("which notify-send"). and_return("foo\n") end context "with options passed at initialization" do let(:options) { { image: "/tmp/hello.png", silent: true } } it "uses these options by default" do expect(sheller).to receive(:run) do |command, *arguments| expect(command).to eql "notify-send" expect(arguments).to include "-i", "/tmp/hello.png" expect(arguments).to include "-u", "low" expect(arguments).to include "-t", "3000" expect(arguments).to include "-h", "int:transient:1" end subject.notify("Welcome") end it "overwrites object options with passed options" do expect(sheller).to receive(:run) do |command, *arguments| expect(command).to eql "notify-send" expect(arguments).to include "-i", "/tmp/welcome.png" expect(arguments).to include "-u", "low" expect(arguments).to include "-t", "3000" expect(arguments).to include "-h", "int:transient:1" end subject.notify("Welcome", image: "/tmp/welcome.png") end it "uses the title provided in the options" do expect(sheller).to receive(:run) do |command, *arguments| expect(command).to eql "notify-send" expect(arguments).to include "Welcome" expect(arguments).to include "test title" end subject.notify("Welcome", title: "test title") end it "converts notification type failed to normal urgency" do expect(sheller).to receive(:run) do |command, *arguments| expect(command).to eql "notify-send" expect(arguments).to include "-u", "normal" end subject.notify("Welcome", type: :failed) end it "converts notification type pending to low urgency" do expect(sheller).to receive(:run) do |command, *arguments| expect(command).to eql "notify-send" expect(arguments).to include "-u", "low" end subject.notify("Welcome", type: :pending) end end context "without additional options" do it "shows the notification with the default options" do expect(sheller).to receive(:run) do |command, *arguments| expect(command).to eql "notify-send" expect(arguments).to include "-i", "/tmp/welcome.png" expect(arguments).to include "-u", "low" expect(arguments).to include "-t", "3000" expect(arguments).to include "-h", "int:transient:1" end subject.notify("Welcome", image: "/tmp/welcome.png") end end context "with additional options" do it "can override the default options" do expect(sheller).to receive(:run) do |command, *arguments| expect(command).to eql "notify-send" expect(arguments).to include "-i", "/tmp/wait.png" expect(arguments).to include "-u", "critical" expect(arguments).to include "-t", "5" end subject.notify( "Waiting for something", type: :pending, image: "/tmp/wait.png", t: 5, u: :critical ) end end end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/rb_notifu_spec.rb000066400000000000000000000074731352050573500243720ustar00rootroot00000000000000require "notiffany/notifier/rb_notifu" module Notiffany class Notifier RSpec.describe Notifu do let(:options) { { title: "Hello" } } let(:os) { "solaris" } subject { described_class.new(options) } before do allow(Kernel).to receive(:require) allow(RbConfig::CONFIG).to receive(:[]).with("host_os") { os } stub_const "Notifu", double end describe "#initialize" do context "host is not supported" do let(:os) { "darwin" } it "fails" do expect { subject }.to raise_error(Base::UnsupportedPlatform) end end context "host is supported" do let(:os) { "mswin" } it "requires rb-notifu" do expect(Kernel).to receive(:require).with("rb-notifu") subject end end end describe "#notify" do let(:os) { "mswin" } context "with options passed at initialization" do it "uses these options by default" do expect(::Notifu).to receive(:show).with( time: 3, icon: false, baloon: false, nosound: false, noquiet: false, xp: false, title: "Hello", type: :info, image: "/tmp/welcome.png", message: "Welcome to Guard" ) subject.notify("Welcome to Guard", image: "/tmp/welcome.png") end it "overwrites object options with passed options" do expect(::Notifu).to receive(:show).with( hash_including( time: 3, icon: false, baloon: false, nosound: false, noquiet: false, xp: false, title: "Welcome", type: :info, image: "/tmp/welcome.png", message: "Welcome to Guard" ) ) subject.notify("Welcome to Guard", title: "Welcome", image: "/tmp/welcome.png") end end context "without additional options" do it "shows the notification with the default options" do expect(::Notifu).to receive(:show).with( time: 3, icon: false, baloon: false, nosound: false, noquiet: false, xp: false, title: "Welcome", type: :info, image: "/tmp/welcome.png", message: "Welcome to Guard" ) subject.notify("Welcome to Guard", title: "Welcome", image: "/tmp/welcome.png") end end context "with additional options" do it "can override the default options" do expect(::Notifu).to receive(:show).with( time: 5, icon: true, baloon: true, nosound: true, noquiet: true, xp: true, title: "Waiting", type: :warn, image: "/tmp/wait.png", message: "Waiting for something" ) subject.notify("Waiting for something", time: 5, icon: true, baloon: true, nosound: true, noquiet: true, xp: true, title: "Waiting", type: :pending, image: "/tmp/wait.png" ) end end end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/terminal_notifier_spec.rb000066400000000000000000000052241352050573500261050ustar00rootroot00000000000000require "notiffany/notifier/terminal_notifier" module Notiffany RSpec.describe Notifier::TerminalNotifier do let(:options) { {} } let(:os) { "solaris" } subject { described_class.new(options) } before do allow(Kernel).to receive(:require) allow(RbConfig::CONFIG).to receive(:[]).with("host_os") { os } stub_const "TerminalNotifier::Guard", double(available?: true) end describe ".available?" do context "host is not supported" do let(:os) { "mswin" } it "fails" do expect { subject }.to raise_error(Notifier::Base::UnavailableError) end end context "host is supported" do let(:os) { "darwin" } it "works" do subject end end end describe "#notify" do let(:os) { "darwin" } context "with options passed at initialization" do let(:options) { { title: "Hello", silent: true } } it "uses these options by default" do expect(TerminalNotifier::Guard).to receive(:execute). with(false, title: "Hello", type: :success, message: "any message") subject.notify("any message") end it "overwrites object options with passed options" do expect(::TerminalNotifier::Guard).to receive(:execute). with( false, title: "Welcome", type: :success, message: "any message") subject.notify("any message", title: "Welcome") end end it "should call the notifier." do expect(::TerminalNotifier::Guard).to receive(:execute). with(false, title: "any title", type: :success, message: "any message") subject.notify("any message", title: "any title") end it "should allow the title to be customized" do expect(::TerminalNotifier::Guard).to receive(:execute). with(false, title: "any title", message: "any message", type: :error) subject.notify("any message", type: :error, title: "any title") end context "without a title set" do it "should show the app name in the title" do expect(::TerminalNotifier::Guard).to receive(:execute). with(false, title: "FooBar Success", type: :success, message: "any message") # TODO: why would anyone set the title explicitly to nil? and also # expect it to be set to a default value? subject.notify("any message", title: nil, app_name: "FooBar") end end end end end notiffany-0.1.3/spec/lib/notiffany/notifier/terminal_title_spec.rb000066400000000000000000000023031352050573500254020ustar00rootroot00000000000000require "notiffany/notifier/terminal_title" RSpec.describe Notiffany::Notifier::TerminalTitle do let(:options) { { title: "Hello" } } let(:os) { "solaris" } subject { described_class.new(options) } before do allow(Kernel).to receive(:require) allow(RbConfig::CONFIG).to receive(:[]).with("host_os") { os } end describe "#notify" do context "with options passed at initialization" do it "uses these options by default" do expect(STDOUT).to receive(:puts).with("\e]2;[Hello] first line\a") subject.notify("first line\nsecond line\nthird") end it "overwrites object options with passed options" do expect(STDOUT).to receive(:puts).with("\e]2;[Welcome] first line\a") subject.notify("first line\nsecond line\nthird", title: "Welcome") end end it "set title + first line of message to terminal title" do expect(STDOUT).to receive(:puts).with("\e]2;[any title] first line\a") subject.notify("first line\nsecond line\nthird", title: "any title") end end describe ".turn_off" do it "clears the terminal title" do expect(STDOUT).to receive(:puts).with("\e]2;\a") subject.turn_off end end end notiffany-0.1.3/spec/lib/notiffany/notifier/tmux_spec.rb000066400000000000000000000467701352050573500234030ustar00rootroot00000000000000require "notiffany/notifier/tmux" module Notiffany class Notifier RSpec.describe Tmux::Client do let(:sheller) { class_double("Shellany::Sheller") } subject { described_class.new(nil) } before do stub_const("Shellany::Sheller", sheller) end describe ".version" do context "when tmux is not installed" do it "fails" do allow(sheller).to receive(:stdout).and_return(nil) expect do described_class.version end.to raise_error(Base::UnavailableError) end end context "when 'tmux -v' doesn't contain float-like string" do it "fails" do allow(sheller).to receive(:stdout).and_return('master') expect do described_class.version end.to raise_error(Base::UnavailableError) end end end describe "#clients" do context "when :all is given" do subject { described_class.new(:all) } it "removes null terminal" do allow(sheller).to receive(:stdout). with("tmux list-clients -F '\#{client_tty}'") do "/dev/ttys001\n/dev/ttys000\n(null)\n" end clients = subject.clients expect(clients).to include "/dev/ttys001" expect(clients).to include "/dev/ttys000" expect(clients).not_to include "(null)" end end end describe "#display" do it "displays text in given area" do expect(sheller).to receive(:run).with("tmux display 'foo'") subject.display_message("foo") end context "when displaying on all clients" do subject { described_class.new(:all) } it "displays on every client" do allow(sheller).to receive(:stdout). with("tmux list-clients -F '\#{client_tty}'") do "/dev/ttys001\n" end expect(sheller).to receive(:run) .with("tmux display -c /dev/ttys001 'foo'") subject.display_message("foo") end end end describe "#message_fg=" do it "sets message fg color" do expect(sheller).to receive(:run).with("tmux set -q message-fg green") subject.message_fg = "green" end end describe "#message_bg=" do it "sets message bg color" do expect(sheller).to receive(:run).with("tmux set -q message-bg white") subject.message_bg = "white" end end describe "#display_time=" do it "sets display time" do expect(sheller).to receive(:run).with("tmux set -q display-time 5000") subject.display_time = 5000 end end describe "#title=" do it "sets terminal title" do expect(sheller).to receive(:run). with("tmux set -q set-titles-string 'foo'") subject.title = "foo" end end end RSpec.describe Tmux::Session do let(:all) { instance_double(Tmux::Client) } let(:tty) { instance_double(Tmux::Client) } let(:sheller) { class_double("Shellany::Sheller") } before do allow(Tmux::Client).to receive(:new).with(:all).and_return(all) allow(Tmux::Client).to receive(:new).with("tty").and_return(tty) stub_const("Shellany::Sheller", sheller) end describe "#start" do before do allow(all).to receive(:clients).and_return(%w(tty)) allow(tty).to receive(:parse_options).and_return({}) end it "sets options" do subject end end describe "#close" do before do allow(all).to receive(:clients).and_return(%w(tty)) allow(tty).to receive(:parse_options).and_return({}) end it "restores the tmux options" do allow(tty).to receive(:unset).with("status-left-bg", nil) allow(tty).to receive(:unset).with("status-left-fg", nil) allow(tty).to receive(:unset).with("status-right-bg", nil) allow(tty).to receive(:unset).with("status-right-fg", nil) allow(tty).to receive(:unset).with("message-bg", nil) allow(tty).to receive(:unset).with("message-fg", nil) allow(tty).to receive(:unset).with("display-time", nil) allow(tty).to receive(:unset).with("display-time", nil) subject.close end end end RSpec.describe Tmux do let(:tmux_version) { 1.7 } let(:options) { {} } let(:os) { "solaris" } let(:tmux_env) { true } subject { described_class.new(options) } let(:version) { 1.7 } let(:client) { instance_double(Tmux::Client) } let(:session) { instance_double(Tmux::Session) } let(:sheller) { class_double("Shellany::Sheller") } before do allow(Kernel).to receive(:require) allow(RbConfig::CONFIG).to receive(:[]).with("host_os") { os } allow(ENV).to receive(:key?).with("TMUX").and_return(tmux_env) allow(Tmux::Client).to receive(:new).and_return(client) allow(Tmux::Client).to receive(:version).and_return(version) allow(Tmux::Session).to receive(:new).and_return(session) allow(session).to receive(:close) stub_const("Shellany::Sheller", sheller) end after do described_class.send(:_end_session) if described_class.send(:_session) end describe "#initialize" do context "when the TMUX environment variable is set" do let(:tmux_env) { true } context "with a recent version of tmux" do let(:version) { 1.8 } it "works" do subject end end context "with an outdated version of tmux" do let(:version) { 1.6 } it "fails" do expect { subject }. to raise_error( Base::UnavailableError, /way too old \(1.6\)/ ) end end context "without tmux" do it "fails" do allow(Tmux::Client).to receive(:version). and_raise(Base::UnavailableError, "Could not find tmux") expect { subject }. to raise_error( Base::UnavailableError, "Could not find tmux" ) end end end context "when the TMUX environment variable is not set" do let(:tmux_env) { false } it "fails" do expect { subject }. to raise_error( Base::UnavailableError, /only available inside a TMux session/ ) end end end describe "#notify" do before do allow(client).to receive(:set).with("status-left-bg", anything) allow(client).to receive(:display_time=) allow(client).to receive(:message_fg=) allow(client).to receive(:message_bg=) allow(client).to receive(:display_message) end context "with options passed at initialization" do let(:options) do { success: "rainbow", silent: true, starting: "vanilla" } end it "uses these options by default" do expect(client).to receive(:set).with("status-left-bg", "rainbow") subject.notify("any message", type: :success) end it "overwrites object options with passed options" do expect(client).to receive(:set).with("status-left-bg", "black") subject.notify("any message", type: :success, success: "black") end it "uses the initialization options for custom types by default" do expect(client).to receive(:set).with("status-left-bg", "vanilla") subject.notify("any message", type: :starting) end end it "sets the tmux status bar color to green on success" do expect(client).to receive(:set).with("status-left-bg", "green") subject.notify("any message", type: :success) end context "when success: black is passed in as an option" do let(:options) { { success: "black" } } it "on success it sets the tmux status bar color to black" do expect(client).to receive(:set).with("status-left-bg", "black") subject.notify("any message", options.merge(type: :success)) end end it "sets the tmux status bar color to red on failure" do expect(client).to receive(:set).with("status-left-bg", "red") subject.notify("any message", type: :failed) end it "should set the tmux status bar color to yellow on pending" do expect(client).to receive(:set).with("status-left-bg", "yellow") subject.notify("any message", type: :pending) end it "sets the tmux status bar color to green on notify" do expect(client).to receive(:set).with("status-left-bg", "green") subject.notify("any message", type: :notify) end it "sets the tmux status bar color to default color on a custom type" do expect(client).to receive(:set).with("status-left-bg", "black") subject.notify("any message", type: :custom, default: "black") end it "sets the tmux status bar color to default color on a custom type" do expect(client).to receive(:set).with("status-left-bg", "green") subject.notify("any message", type: :custom) end it "sets the tmux status bar color to passed color on a custom type" do expect(client).to receive(:set).with("status-left-bg", "black") subject.notify("any message", type: :custom, custom: "black") end context "when right status bar is passed in as an option" do it "should set the right tmux status bar color on success" do expect(client).to receive(:set).with("status-right-bg", "green") subject.notify("any message", color_location: "status-right-bg") end end it "does not change colors when the change_color flag is disabled" do expect(client).to_not receive(:set) subject.notify("any message", change_color: false) end it "calls display_message if the display_message flag is set" do expect(client).to receive(:display_message). with("Notiffany - any message") subject.notify("any message", type: :notify, display_message: true) end context "when the display_message flag is not set" do it "does not call display_message" do expect(client).to_not receive(:display_message) subject.notify("any message") end end it "calls display_title if the display_title flag is set" do expect(client).to receive(:title=).with("Notiffany - any message") subject.notify("any message", type: :notify, display_title: true) end it "does not call display_title if the display_title flag is not set" do expect(client).to_not receive(:display) subject.notify("any message") end context "when color_location is passed with an array" do let(:options) do { color_location: %w(status-left-bg pane-border-fg) } end it "should set the color on multiple tmux settings" do expect(client).to receive(:set).with("status-left-bg", "green") expect(client).to receive(:set).with("pane-border-fg", "green") subject.notify("any message", options) end end context "with display_title option" do let(:options) do { success: "rainbow", silent: true, starting: "vanilla", display_title: true } end before do allow(client).to receive(:title=) allow(client).to receive(:set).with("status-left-bg", anything) end it "displays the title" do expect(client).to receive(:title=).with("any title - any message") subject.notify "any message", type: "success", title: "any title" end it "shows only the first line of the message" do expect(client).to receive(:title=).with("any title - any message") subject.notify( "any message\nline two", type: "success", title: "any title" ) end context "with success message type options" do it "formats the message" do expect(client).to receive(:title=). with("[any title] => any message") subject.notify( "any message\nline two", options.merge( type: "success", title: "any title", success_title_format: "[%s] => %s", default_title_format: "(%s) -> %s" ) ) end end context "with pending message type options" do it "formats the message" do expect(client).to receive(:title=). with("[any title] === any message") subject.notify( "any message\nline two", type: "pending", title: "any title", pending_title_format: "[%s] === %s", default_title_format: "(%s) -> %s" ) end end context "with failed message type options" do it "formats the message" do expect(client).to receive(:title=). with("[any title] <=> any message") subject.notify( "any message\nline two", type: "failed", title: "any title", failed_title_format: "[%s] <=> %s", default_title_format: "(%s) -> %s" ) end end end it "sets the display-time" do expect(client).to receive(:display_time=).with(3000) subject.notify( "any message", type: "success", display_message: true, title: "any title", timeout: 3) end it "displays the message" do expect(client).to receive(:display_message). with("any title - any message") subject.notify( "any message", display_message: true, type: "success", title: "any title" ) end it "handles line-breaks" do expect(client).to receive(:display_message). with("any title - any message xx line two") subject.notify( "any message\nline two", type: "success", display_message: true, title: "any title", line_separator: " xx ") end context "with success message type options" do it "formats the message" do expect(client).to receive(:display_message). with("[any title] => any message - line two") subject.notify( "any message\nline two", type: "success", title: "any title", display_message: true, success_message_format: "[%s] => %s", default_message_format: "(%s) -> %s") end it "sets the foreground color based on the type for success" do allow(client).to receive(:message_fg=).with("green") subject.notify( "any message", type: "success", title: "any title", display_message: true, success_message_color: "green") end it "sets the background color" do allow(client).to receive(:set).with("status-left-bg", :blue) allow(client).to receive(:message_bg=).with("blue") subject.notify( "any message", type: "success", title: "any title", success: :blue) end end context "with pending message type options" do let(:notify_opts) do { type: "pending", title: "any title", display_message: true } end before do end it "formats the message" do # expect(sheller).to receive(:run). # with("tmux display"\ # ' \'\'').once {} # expect(client).to receive(:display_message). with("[any title] === any message - line two") subject.notify( "any message\nline two", notify_opts.merge( pending_message_format: "[%s] === %s", default_message_format: "(%s) -> %s") ) end it "sets the foreground color" do expect(client).to receive(:message_fg=).with("blue") subject.notify( "any message", notify_opts.merge(pending_message_color: "blue") ) end it "sets the background color" do expect(client).to receive(:message_bg=).with(:white) subject.notify("any message", notify_opts.merge(pending: :white)) end end context "with failed message type options" do let(:notify_opts) do { type: "failed", title: "any title", display_message: true, failed_message_color: "red" } end before do allow(client).to receive(:set).with("status-left-bg", anything) allow(client).to receive(:message_fg=) allow(client).to receive(:message_bg=) allow(client).to receive(:display_message) end it "formats the message" do expect(client).to receive(:display_message). with("[any title] <=> any message - line two") subject.notify( "any message\nline two", notify_opts.merge( failed_message_format: "[%s] <=> %s", default_message_format: "(%s) -> %s") ) end it "sets the foreground color" do expect(client).to receive(:message_fg=).with("red") subject.notify("any message", notify_opts) end it "sets the background color" do expect(client).to receive(:message_bg=).with(:black) subject.notify("any message", notify_opts.merge(failed: :black)) end end end describe "#turn_on" do context "when on" do before do subject.turn_on end it "fails" do expect do subject.turn_on end.to raise_error("Already turned on!") end end end describe "#turn_off" do context "when on" do before do subject.turn_on end it "closes the session" do expect(session).to receive(:close) subject.turn_off end end context "when off" do before do subject.turn_on subject.turn_off end it "fails" do expect do subject.turn_off end.to raise_error("Already turned off!") end end end end end end notiffany-0.1.3/spec/lib/notiffany/notifier_spec.rb000066400000000000000000000277511352050573500224040ustar00rootroot00000000000000require "notiffany/notifier" module Notiffany RSpec.describe Notifier, exclude_stubs: [Nenv, Notifier::Env] do let(:options) { { notify: enabled } } subject { described_class.new(options) } let(:logger) { instance_double(Logger, :level= => nil, debug: nil) } let(:enabled) { true } # Use tmux as base, because it has both :turn_on and :turn_off %w(foo bar baz).each do |name| let(name.to_sym) do class_double( "Notiffany::Notifier::Tmux", name: name ) end end let(:foo_object) { instance_double("Notiffany::Notifier::Tmux") } let(:bar_object) { instance_double("Notiffany::Notifier::Tmux") } let(:env) { instance_double(Notifier::Env) } let(:detected) { instance_double("Notiffany::Notifier::Detected") } before do allow(Notifier::Env).to receive(:new).with("notiffany").and_return(env) allow(Logger).to receive(:new).and_return(logger) # DEFAULTS FOR TESTS allow(env).to receive(:notify?).and_return(true) allow(env).to receive(:notify_active?).and_return(false) allow(env).to receive(:notify_active=) allow(env).to receive(:notify_pid).and_return($$) allow(env).to receive(:notify_pid=).with($$) allow(described_class::Detected).to receive(:new). with(described_class::SUPPORTED, 'notiffany', logger). and_return(detected) allow(detected).to receive(:add) allow(detected).to receive(:reset) allow(detected).to receive(:detect) allow(detected).to receive(:available).and_return([foo_object]) allow(foo_object).to receive(:title).and_return("Foo") allow(bar_object).to receive(:title).and_return("Bar") end after do # This is ok, because it shows singletons are NOT ok described_class.instance_variable_set(:@detected, nil) end describe "#initialize" do before do allow(env).to receive(:notify?).and_return(env_enabled) end context "when enabled with environment" do let(:env_enabled) { true } context "when enabled with options" do let(:options) { { notify: true } } it "assigns a pid" do expect(env).to receive(:notify_pid=).with($$) subject end it "autodetects" do expect(detected).to receive(:detect) subject end end context "when no options given" do let(:options) { {} } it "assigns a pid" do expect(env).to receive(:notify_pid=).with($$) subject end it "autodetects" do expect(detected).to receive(:detect) subject end end context "when disabled with options" do let(:options) { { notify: false } } it "assigns a pid anyway" do expect(env).to receive(:notify_pid=).with($$) subject end it "does not autodetect" do expect(detected).to_not receive(:detect) subject end end end context "when disabled with environment" do let(:env_enabled) { false } pending end context "with custom notifier config" do let(:env_enabled) { true } let(:notifiers) { { foo: { bar: :baz } } } let(:options) { { notifiers: notifiers } } before do allow(detected).to receive(:available).and_return([]) allow(env).to receive(:notify?).and_return(enabled) end context "when child process" do let(:enabled) { true } before { allow(env).to receive(:notify_pid).and_return($$ + 100) } it "works" do subject end end context "when not connected" do context "when disabled" do let(:enabled) { false } it "does not add anything" do expect(detected).to_not receive(:add) subject end end context "when enabled" do let(:enabled) { true } context "when supported" do let(:name) { foo } context "when available" do it "adds the notifier to the notifications" do expect(detected).to receive(:add).with(:foo, bar: :baz) subject end end end end end context "when connected" do before do allow(env).to receive(:notify?).and_return(enabled) end context "when disabled" do let(:enabled) { false } it "does not add anything" do expect(detected).to_not receive(:add) subject end end context "when enabled" do let(:enabled) { true } context "when :off" do let(:notifiers) { { off: {} } } it "turns off the notifier" do expect(subject).to_not be_active end end context "when supported" do let(:name) { foo } context "when available" do it "adds the notifier to the notifications" do expect(detected).to receive(:add). with(:foo, bar: :baz) subject end end end end end end end describe ".disconnect" do before do allow(env).to receive(:notify_pid=) end it "resets detector" do expect(detected).to receive(:reset) subject.disconnect end it "reset the pid env var" do expect(env).to receive(:notify_pid=).with(nil) subject.disconnect end end describe ".turn_on" do let(:options) { {} } before do allow(detected).to receive(:available).and_return(available) subject allow(env).to receive(:notify_active?).and_return(true) subject.turn_off allow(env).to receive(:notify_active?).and_return(false) end context "with available notifiers" do let(:available) { [foo_object] } context "when a child process" do before { allow(env).to receive(:notify_pid).and_return($$ + 100) } it { expect { subject.turn_on }.to raise_error(/Only notify()/) } end context "without silent option" do let(:options) { { silent: false } } it "shows the used notifications" do expect(logger).to receive(:debug). with "Notiffany is using Foo to send notifications." subject.turn_on(options) end end context "with silent option" do let(:options) { { silent: true } } it "does not show activated notifiers" do expect(logger).to_not receive(:info) subject.turn_on(options) end end end context "without available notifiers" do let(:available) { [] } it "sets mode to active" do expect(env).to receive(:notify_active=).with(true) subject.turn_on(options) end end end describe ".turn_off" do before do allow(env).to receive(:notify?).and_return(true) allow(detected).to receive(:available).and_return(available) end context "with no available notifiers" do let(:available) { [] } it "is not active" do subject expect(subject).to_not be_active end end context "with available notifiers" do let(:available) { [foo_object] } before do subject end context "when a child process" do before { allow(env).to receive(:notify_pid).and_return($$ + 100) } it { expect { subject.turn_off }.to raise_error(/Only notify()/) } end it "turns off each notifier" do allow(env).to receive(:notify_active?).and_return(true) expect(foo_object).to receive(:turn_off) subject.turn_off end end end describe ".enabled?" do before do allow(env).to receive(:notify?).and_return(enabled) end context "when enabled" do let(:enabled) { true } it { is_expected.to be_enabled } end context "when disabled" do let(:enabled) { false } it { is_expected.not_to be_enabled } end end describe ".notify" do context "with multiple notifiers" do before do allow(detected).to receive(:available). and_return([foo_object, bar_object]) allow(foo).to receive(:new).with(color: true).and_return(foo_object) allow(bar).to receive(:new).with({}).and_return(bar_object) allow(env).to receive(:notify?).and_return(enabled) end # TODO: deprecate context "when not connected" do let(:enabled) { true } before do allow(env).to receive(:notify_active?).and_return(false) end context "when a child process" do before { allow(env).to receive(:notify_pid).and_return($$ + 100) } before do allow(foo_object).to receive(:notify) allow(bar_object).to receive(:notify) end it "sends notifications" do expect(foo_object).to receive(:notify).with("Hello", foo: "bar") expect(bar_object).to receive(:notify).with("Hello", foo: "bar") subject.notify("Hello", foo: "bar") end it "shows a deprecation message" do pending expect(logger).to receive(:deprecation). with(/Notifier.notify\(\) without a prior Notifier.connect/) subject.notify("Hello", foo: "bar") end end end context "when connected" do before do subject allow(env).to receive(:notify_active?).and_return(enabled) end context "when enabled" do let(:enabled) { true } it "sends notifications" do expect(foo_object).to receive(:notify).with("Hello", foo: "bar") expect(bar_object).to receive(:notify).with("Hello", foo: "bar") subject.notify("Hello", foo: "bar") end context "when a child process" do before { allow(env).to receive(:notify_pid).and_return($$ + 100) } it "sends notifications" do expect(foo_object).to receive(:notify).with("Hello", foo: "bar") expect(bar_object).to receive(:notify).with("Hello", foo: "bar") subject.notify("Hello", foo: "bar") end end end context "when disabled" do let(:enabled) { false } it "does not send notifications" do expect(foo_object).to_not receive(:notify) expect(bar_object).to_not receive(:notify) subject.notify("Hi to everyone") end context "when a child process" do before { allow(env).to receive(:notify_pid).and_return($$ + 100) } it "sends notifications" do expect(foo_object).to_not receive(:notify) expect(bar_object).to_not receive(:notify) subject.notify("Hello", foo: "bar") end end end end end end describe "#available" do context "when connected" do let(:options) { { notify: true } } before do subject allow(env).to receive(:notify_active?).and_return(true) allow(detected).to receive(:available).and_return(available) end context "with available notifiers" do let(:available) { [foo_object, bar_object] } it "returns a list of available notifier info" do expect(subject.available).to eq([foo_object, bar_object]) end end end end end end notiffany-0.1.3/spec/notiffany_spec.rb000066400000000000000000000001621352050573500200020ustar00rootroot00000000000000RSpec.describe Notiffany do it 'has a version number' do expect(Notiffany::VERSION).not_to be nil end end notiffany-0.1.3/spec/spec_helper.rb000066400000000000000000000010271352050573500172650ustar00rootroot00000000000000RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end config.mock_with :rspec do |mocks| mocks.verify_partial_doubles = true end config.filter_run :focus config.run_all_when_everything_filtered = true config.disable_monkey_patching! config.warnings = true config.default_formatter = 'doc' if config.files_to_run.one? # config.profile_examples = 10 config.order = :random Kernel.srand config.seed end