olive-1.3/0000755000175000017500000000000010626577224011172 5ustar mdximdxiolive-1.3/docs/0000755000175000017500000000000010626577224012122 5ustar mdximdxiolive-1.3/docs/manual.css0000644000175000017500000000135110366246443014106 0ustar mdximdxip.warn { margin-left: 4em; margin-right: 4em; background-color: #fc3; border: solid #000 thin; font-weight: bold; padding: 0.5em; } pre { margin-left: 4em; margin-right: 4em; background-color: #ddd; } pre.screen { color: #fff; background-color: #000; font-family: monospace; border: solid #c0b9c0 medium; -moz-border-radius: 0.75em; } pre.screen span { background-color: #00f; color: #fff; font-weight: bold; } pre.screen span.rv { background-color: #fff; color: #000; } code { color: #04a; font-weight: bold; } olive-1.3/docs/logo.png0000644000175000017500000006320210370730036013557 0ustar mdximdxiPNG  IHDRj ;iCCPiccxڝSwX>eVBl"#Ya@Ņ VHUĂ H(gAZU\8ܧ}zy&j9R<:OHɽH gyx~t?op.$P&W " R.TSd ly|B" I>ةآ(G$@`UR,@".Y2GvX@`B, 8C L0ҿ_pH˕͗K3w!lBa)f "#HL 8?flŢko">!N_puk[Vh]3 Z zy8@P< %b0>3o~@zq@qanvRB1n#Dž)4\,XP"MyRD!ɕ2 w ONl~Xv@~- g42y@+͗\LD*A aD@ $<B AT:18 \p` Aa!:b""aH4 Q"rBj]H#-r9\@ 2G1Qu@Ơst4]k=Kut}c1fa\E`X&cX5V5cX7va$^lGXLXC%#W 1'"O%zxb:XF&!!%^'_H$ɒN !%2I IkHH-S>iL&m O:ňL $RJ5e?2BQͩ:ZImvP/S4u%͛Cˤ-Кigih/t ݃EЗkw Hb(k{/LӗT02goUX**|:V~TUsU?y TU^V}FUP թU6RwRPQ__c FHTc!2eXBrV,kMb[Lvv/{LSCsfffqƱ9ٜJ! {--?-jf~7zھbrup@,:m:u 6Qu>cy Gm7046l18c̐ckihhI'&g5x>fob4ekVyVV׬I\,mWlPW :˶vm))Sn1 9a%m;t;|rtuvlp4éĩWggs5KvSmnz˕ҵܭm=}M.]=AXq㝧/^v^Y^O&0m[{`:>=e>>z"=#~~~;yN`k5/ >B Yroc3g,Z0&L~oL̶Gli})*2.QStqt,֬Yg񏩌;jrvgjlRlc웸xEt$ =sl3Ttcܢ˞w|/ǩ pHYs.#.#x?vzTXtRaw profile type iptcxڥK$;n.8Uh xXf=Azg*{ '?OHBx*!*t$p{ *T/`H椙QI3/?7g/`1П2m*oUNЙXϳ)1"0_DItU?$}[5ˤyw2cOˁ~3a?\o~_XJy~;!9_!_1`Sn')&!?>C]XA;#S;5j9㝌7÷/0|X g^-}wo'Deiu1!xJVj=Va Φk۫B"O|ol @Ht-@b{-C`n0/Zo68[^v~^\'v[E>,IwJvx/֘ `~oe0nu w b ?On2T+C?Jx/bWR/c9 o`jHIJڼY}Wdn1hn*J^­3x J`(Q pNR]曮K0.|:k{%vט;,kY_avw{ %QryK$t! k<(a2Wkכ0-8.9W巬v7H\`X|ww|6c2\ 7U|&y qq'Xn2%1E*h]2ws{Hw^\^x`GW>e|"Rv*iq k36h.足O瓕O+;ctT6k_1kEL\Yޕ5Py-LA%u"덩NN+ʯ|}j^k=%/@,QcHV׉7?>_8zp _X^fXջz~7g')!I8Jt՞M|zzIG횷49=9N< lO{9.O3;_r-s=FnKNn#θ*`f/'ƞNv5gVPY'ohIpN&oDw`ͫ(1<1{&5]st1hM8C?Mz~=Ჿ+zC30Iw\noi\cU}毽 T>gsШqTq#q|E|'ynn/ac<Ȱ-/{ٮDXG[ɻxF+peoNK3{x/֨_;v΁|pF=.޹d yjT6<1bT 4d{Weֽ";r[䤶nXDye {ϩΧwT)7V=-63]{~).}Od4GOҟai#ps=)$n~ m0F\c'TE&IF}oo!I\v]%>,"YY|V:LOr]'g<~M#woNRpӇK|8o} kDˋ'T{-'Iݰ鯢E5U] RR~Lˇn#W}7w`6{9 =-U`<~mz6pz@~Vs.FZ۷g@Sڭn3L4]w h[L@,}t99q'qu Lc#vÍZxމʈ+/.ȯdr>nQI46qi{l -R';»)M]#!F’eU'~Mz,I1 ݌lbdc 8TJ~\98lhq3=\#q^ݚ5Q-Ӹ-.6mrėCIK;=p;(?y 9`pX;~᫇ m(`'tAF֙LP_;+Ih$E9͓klnϣXw8}vuކ]>p~x7Lƙ֤UO4}quqH+p:.̑/Ĭ$-|69J{}׎w.iʝh/#TlBW5~w ۱UDB/r_[uͮcv )m{tӧrHӽ/EiJJxؿ9Yk|ڴbx9!v<{>YXSxJT+_aG{EIX{ G5Vkcgٱ7XCZ~16yymi1+zVypٚ3)=^uÜUs̮yێkv|jB[LCq ENBXW~0ȩ fkF-wxg]~㘫3\ʢbebloم#cd޸8XExԦV O.'*R 33&ˏk$g9y3WU{cic`-h?z5SZ ]s~upߝ~d|${\#T4v`I8|zJ'e;Eyyo;bӴ&^t DG#R nޮ9?ɤ6Ԯ;831Qw7|pu`k` [2awUgw<ӪQAXThk.Gv/JϖVH0@^L}IU+z 7^/"b@~BAˋ]uw >7c-W۳w\xi3Oc6n&L y^Ũnsr3fy/t gtxb֨o6# Vl#5Lo ?Jퟃ ]\bVW8`y:]n]veggKse5g1jw*~k}삻9$SLq2C)of\ס`qfzf SمqXxgx)['tNw+܉ѷv0w|[~~ۻrARip]ڏCڞ]a!B_scxxb>8/ǴJ[jmznH]:j3wSξQqxGN#W|]iҫgr? gwuJ_):ƟA iwX׈)A&!Qc+;zwѿu,Y7Zinkmn)Mo ]T3=$g& F_ͮ*I?w?{ņ]GkL8LZRMY[|nM7E%hlhgMX7 w S/i@6[P}ph:t? BØ>QrX,lLm]4x?8h68;07`Ϙi @ϣw;@ck!P *>)[HU`En"G7_ - . = h3"ݬB 1E ooKȆrXFiM^bjՙ3t!? M/e v~WM24^!Ƚ_A̋@ k  hNHm5Xmo &h _|hEThuc"98"fFIDATx]wU]sM HU@AĂ{$cƚ`%M]" &"3?}ef{iwyޞu{mbfD>m;3 } "E ɩߥ㺵 l"h$5[qMDĦ~]:"}VԡE<5Iƻ'{Bo_bDTZ`u.gC:!x"lDw ^8q1y`h]xHlLf~:_K$hۻq.u`D@jօIH.]i| #Gh "6bW._MkݷR WkIDD,/ .7;VB#Dh`a9UAq!Bsശҡ;3}"HPن\g;  C:P!=p\ ^/E A.7tgj+?y$ws8"4  =mז ֋ptg\Ů˓$6}%#4\){CCl GF=6ҋdL# $-S21.^EPA##GhfR1Nw4by O˨X B;BD##DtЎ=B;Bn['sE@Gimht، Uǘ ,">mI3 kyе0>Ő[TeȀ@a}#澷=^'/5/PDDѹ/m5zlO=S(FRqeA `Rk P'I@̖Q^+| **07R^fS߰ @OwH(.?P:8^rEo ?CcW C ܋B% D n "ip)HvXåi=xNh1d< @I&X`DRE&8$)/@\LOA.1dv(N ޳eA@wYcĩo5/,LytAG={I@7}3L6:.COD91 \ghgH iqbİCx+gN&H! [ACo( p!Pr5;5\LCO;H 0 Ahk =Н@ׂ4|@/ARu.v>JvmIm]i ݷ02 Bd^w0bSx7n"ұ6K#5"b~~2ߏ璩@h}讝_FMADyĵ<9;9P9b)T-P8́NV;]tovZ I*ʓvWm-5 `f& ݡk hbk׌> "p"g ͥGj c?|~Oc鳈Ȗ d4PDf<1glLl OtyaʓT(y|}k7BmevCwp'T8bDP3΀B6#r+"4r+^LBݿ$زH]v#ֆ-]Թ|rr\?i&(0H_/[#zN U4W6P _E-womz 64Ŷ\}@B2[y}Ow.l5\ٷ ߓǛ$c]AT`D~D5bx2k%3^)(˹\x )!ƛZ–lNzo/Ε]KEfQN7$[f;RN>دU9L^ (  I JP`Ml^~ AݱAr-2 䀔Mwps*Az)P=sl4 yp0`7=nq;v:O in,yB5㝸W)Oa^{~ F0 7ߞ(.]bОjP0L8e^f%=[ {yzyVPN2Mx0hُe^/"bP uHQh^Φla|%%PQR;E}UCsˎkD.aVC(yޤNpf۴a>`|e ⲟH6B^ /)}Z31v/_U:%H(yX<7e/T+øj 5Mr؀J^yz?qW`1Y Nгek ɟR Hj] yv' j{mbSxl};UI̔;P:P8n>xE:tl4m5H]:e2k5r9Y`Z9S?N\Zm]M[{bA f %M}"P|K/+ K]}ypyiAw ЉUөǶ\IʠV"w0޼$$iʢ&G[{_DbjbX:ʝ'OG¼/]3G;..l҆@y/f gU]6CF )f߼oh>]MT[BgYogqc ~Ǧ% e&MWo1(;U;gO2yyj/4*T1gųId|[{"7t<| AȤ ?|IBi.\cr**IW#K##ĺ20*l}}NR)o *[Sjtvت.R~J<6\s8n%)_6>U/MA6N]HO)y\75~*'og gJ|; t'1hOGPɸGϱK?KLDهTk"ŵnEem~A{ u9`:J"T$˷Qr{v@HxuM{$i!;cW~  'K%y)vw^Òv`,/w;&%[.D}FP!2)ގ$LdڑށƝú,yoٯҦqOZH~-EzXFSY#켷ҡ|:F O]&w"@xͳH̊j"]="r!5TԍFzػaw_Q\i\=Q!TxO'*`O!鹝Wu*DL7s}sAɧTgûaw b(M$κXR$*{I#T&l n2]E7(S!s *Y}SHxRą@Ecs>=U{WϐNX!jMn=N>{^6Y{OEx|+l&{HKI$fcYx7W'_O[  la:zFڒq?&!y,Xl2<}](juri\&i?.6/^ -̹ {VQL'y}κ ;qDcD_пiO$s xVOx.V[ofE-A~3n usKOwC<\UYp |US.xIOo3t$|̖L󸪔H 1`7#1-V@71Q\([s /z Sg? l|g0)\т֬mayo;A~ v0+E=RNp.4znG5Xwv~;m8# sh$5G+'"5?ud5TѶ um긛a-Z@G+ K+c qUWUc!䂺mƈn Tx tT6ݶ.yv̕\]KyO#B(p#[#h~ig촻y_RQں-ǚoZXH ʕĝ$e~Ooռ2.-HHlo'0w4%5Qa\!2ߐ TMLߎ4<{){z*2f&¼rB}k6uVl y )]yzH}Hu|}Lml(]<.̔1Ւ Dz_0D˟5Qʪ˖xRrYKG&M/ ʕT쑄sDYOLR$KpDZ]헯m7a)6ZrD.י3*SOD B^c߸]i؊E.8y5\f-Lgf U낯5+ncH~5g?~+okDHj0]O&}NDu#D>+^itDw:r{;oxIo27$ֈ#JFx+/^܂5{H^cK$Bp3ㅀH~H-aȍ¼/v9NM&@>`?L>SP&+Ox΋)xf~8*)A(TYKYC3-!IH`"ռv YīoyX O|*茎=6} ڎ:rt;CHsZ&Z:ukw#lT ɑ/L}wO!Z9wvKQA f]bc\:#15$v' ( 2&k+t~ !!$qmY)/xb\mU RЭ#PKݶGĐ}c` 6MxI겷}ۏ^F7$f&b<|ur/?1lr'l;v;b>lEkWE3xzz^Ȱ%sE@4mj맻#:!,!.i켷x2P`zUP!Q fV7_c#!)v;Ut$Iox(,9^g?x~St~[k{8rd˰T[b>=!3MŘ̋"Gi٥B/@К`7Rҏ_fڣzy'e ~9ݼ|}Yv_Fko2^' ƞJn,@@:^$j+7k^׮|IE`0ۺg*{ a JXgnb^$T}I$yψ'p_ʕ`5yI?Yx$!] xxF0~?T Vټ;WtkFHLP} +Dq4QbW.\sF`s8֐TT?5ɿvGKybp|, ؒQ{V `K|1_l}>+MزDѬ"\VhL=-0NUy>l}\$j*'_+P<,Z6RedL +'C/G_*=El;嚫 @"ŊW.շ>Uu*7"JTP^GuKԽoU.Kg7o'7BHdRkॳjlH @ ?ɵi; wMgZR5>b Ƹb#楉t)r=|Ws`&!ط9s䄋۶,Dl-. a}J{ 6r$bd'ԋB~;S߼L^) 2e -u.{Ct7t]"u/v;ͻ=Ybør!`-/-o[gԣ;z*vQ< 9c4SM0|Y#W†^DlWB ^t۫-n R {yS710,oe]wP5 3akHW+ޠ^۹2z5W>~Qu3^4gl^Ҹt3)[V/\ᅨ}W"lbfkv:5[$Р=綁#U& vsoT(ubTW놙 OPdk\Ȁ\r3ŰY$="p&p_lzwyȯBQ}3lzjs׊jpI@څb C%Ols)'O ;Ȱb,Mlt$$VѷSl3#Ӏ`ՅxDݟLtHS~k:>W='nr}G$zOڅێ*B##xN5[Nό8Qzxas-II*]zi~bRמ"m?i{ʻux[%)`j='HG7!FSQwu}h9^aQ4M~ < uzf1ԭ7U`R7şM%[ݓF&wo؝X@s0!,k"uOyŁ&HΖeS(V1>x ]fbvb٪ŰsPnq,;<41`dغD!k]]S^nj'3{"2i9tOBhYAbN8g*NevW(\3 QO @1Ӏa._ߕ̔j"!< ^=o9X.."H;بgwUJ2]'oLy="ΎR$. QPYW݉JWL0 y;ݔMjW'ވM DmɴaඁFG]CݦAфzC1h/*cCdfxr:M"[ݎ %w%RO{bpjC%猖'ǟe}Kf -v8PuF}\Nü} ] VSbQP5᢬{|mg+IґM`an?|޼f(>iL X,SD }V">O@uuYD%!(I&/2Ri^p BxNcH}C밚gg?8J ]8T\L+Y_9idg-b0^ޥ<6PyD&ރCo1I7 1h/f^Ic^^un>|̿%E55@#*RKG/FUS;O؝pvŏڪPum Kcii so$兖}47A3=p}# [0LuɕfRKKU ̌ٯ ݼbyEn:2dF@Nh/fx/ÝS/1$s;ްBAzgR eB)^yhVl: Se yv=΢>Fj;)pHٕo~8=bV. n@\ۥI .+ y޴tw2$=;?)G9\y~ֵun-ɨDiz[FSnͪwU!#GrŐܐT\^k} Iیq Z+qa=&ْ'Aٸ"yWlgTsه:v`c`%Ӹ|u_Ե49'y@Xv8"31w ^U-KľTva}ٸI0I<@l uܚG䵫@a@l4u)Fz~{U(>HRH'}^5:o5:teg $ݥ-H떄 GszʑMin~K|j? >*4d|iȕLB* w]ςU䇸.bԩ7V8ǥ+C3uݶBWwwi`=H;Z}{U[.yռ`@W"\QA(~"L`f1`72KBX LRpziAp :՞gNwfH`Ï(;l[ f&6Ld8~9-ؒx*KM]Q~. zm+^P`*k!߅^ypPbsLCJ^,ݻkxͿ e޽LyU:2st3HόN Vwal=.x78k?r6mW(ӝzB># '$xgǠFXhٚC]i84RK'~Okki0O\h}\_^ rsoR߫V=SQU |.YVXҥw"~u[!g51`߻Ȣѝ_m:k5- FIeLb\#-ZW;MnJeS lY :֡Wkr K.N'g pv1FA4120/L PD`fr\y!ex~aA yB w۫ {w-WY !ҕX#RWtAqkMn,ʀ&{B,n޸ ZdC~㠀f3`\d,'bMȋek֛tp![saHHּf 8akH+=ʍM?+FS7HK`%-K^f_F쟓>emKykմEߑy&/RHT3($(슢o6'p7aOqdv`ե|?v.6^< >W@Xc< ٰ+OOۮNٸL/0kʓM4?~$=uXaV~tW@#Zvtoѝ`,>ho==`z Y*RvO_ +H(t lu-I.Ju]4[б{n BP"aLt : lz[ !WXL̸n}c[2ܧ4Ƽr`3x{\]FBB( B+}S@Xl)V@lą޵K܅:{+!k( 3n_MeZXhw W3AM"D(R\Sa>jyu*̕y[252x"~0[}agnf!@i1 VBq^*'O`yAӌ3RO?-v-}&A(R`k^y5 䒋!A]qG {J;쮐/_5++[;f+D,5UHlKlj3{}3f;s=8ADJKʌY5m lSskb w 0eO!dųoE؆7mO ]?́ۚw|^.Em 昮.5H8WEM6= Ŕ߁ٺ@੦{WOcݨpR9dO#dSHB|$0k9a/l 38杄3‚(G27;D4o.K4^֒韴>dY̐ KYt;d4(1n-۟'=ip=$7_zC6 O& d[`]˥?)k`@ TZ1**P2<Go]0֐bs|"9B* !HM1'@=#d& +sXz99Bw> 4 i"t|=m$Dҝ ]BB5$CQnQ[ݝKZ3$$Wi(1W(,dcp#Z3,LB1G l=Xhj&\n'agPmv\vd 5u* rG%!M=m5}n~`jn.谆 HkcbOm`f˵sl ҭREt$;NjZM]Se[.f[~_ޛ}4A:^'1sN FzF?032#T<&"b4:19OlvsG |xy6-MI.b!\m2c5zW~H~2QM3gڤR< =CzY#u4Y;̕-!}LF${+@Sid?־m05ؓJ~yy 򴕠 A\8M˵:foڜZkh:1Cʣ&Qn@N %kydyl4/O#4&?`SoyMw|Wiy-l4[+B) $[#ǝ-dK0ʨsW^G\oh.!E@v7uɵhEgYg]rܹ^ 4\Ss`ߴBMOw)-F)eRp~\S?-ǜqcԣxud1'XI |1r?bnߗ1 ' }Z {]O#n zYB\3 #:[~L3_׮5PIɔl-4C[X Թ ] ;Dpu?lH;] >T酤R;kx{d*\PN*??ѱ7m=Tl+m;ݙ݌x8hZ0vGG15f,˹l%Ja|!VD]COtC+wl 2(s$;Tԕ ^ .L "Q("z{EˣCb8YVJv_ݖ%)*z"{T- uuT(!VA D7!CD##DtЎ.[F]IjDtxe &3BL9G͇Į_c;|Ő0>֜6` 0,#=4kE.ԿّGhthfDgkW32#,ݙ-)uO{qgѶcIz| Ct>剐aNDl<5e忁Yy[rE5B NDl- BA %qp\tKE[_)HɈZ bQ<:BDrm#ԍB[ȵ2"l4xo 6RN[ @r)j^RVP9O}5;dkSԩ7rϕm#&ee^]#v;@w XkR,ؒϹ"@\t'8Kw!M܄gt'bfu^(_V@DZ: 5 "36^13(՞%BDfsi9F&BtVoƵlcW5B;BDm; IENDB`olive-1.3/docs/news.html0000644000175000017500000003266510357072117013770 0ustar mdximdxi User Manual: News

2. Reading News With Olive

(Getting Started) Prev | Index | Next (Setting Options)


2.1 The User Interface

Olive does not mimic GUI-style user interfaces by implementing text-based menus, controls, and the like. Instead it takes programs like mutt as its inspiration and stays out of your way as much as possible. Transient panels and message dialogues aside, the Olive UI is composed of 4 components (listed here top-to-bottomly): Title Bar, List Pane, Status Line, and Story Pane. Only the List Pane and Story Pane can be interacted with in any meaningful way.

Focus is shifted between the List Pane and Story Pane with the 'w' key, but this is rarely needed anymore.

2.1.1 The Titlebar

The titlebar is simply the top line of the window, used to display Olive's name and version number in white-on-blue text.

  |
  | Olive b7                                                  
  |

You can hide the titlebar and have this line of screen real-estate for your own use. See Section 3.2.4 for details on how to do this.

2.1.2 List Pane

The top half of Olive's window is used by the List Pane. This is where the stories from the feeds currently in your feed list are displayed, one story per line. Each line displays the following information:

  1. Title (taken from RSS data)
  2. Feed nickname (taken from feed config)
  3. Age (time since posting, taken from RSS data)
  4. Starred indicator
  5. Read/Unread indicator

Stories are sorted as follows:

  1. New or Unread stories, sorted oldest-to-newest by age
  2. Old stories, sorted newest-to-oldest by age

If there are both new and old stories present in the list, a separator line of dashes will be displayed between them.

                  TITLE                       FEED         AGE   S U
    |------ (Width - 29 chars) -------| |-- 16 chars --| |- 7 -| 1 1

  | Martian rover rolls free of trap    BBC News         00h 52m   @
  | Pacman comes to life virtually      BBC News         00h 38m * @ 
  | Many dead in Nepalese bus blast     BBC News         00h 12m   @
  | ----------------------------------------------------------------
  | China dismisses US Tiananmen call   BBC News         03h 38m * -
  | Japanese firms raise investment     BBC News         04h 12m   -

The age field displays 'HHh MMm' for stories less than one day old, 'DDd HHh' for stories between 1 and 100 days old, '___+99d' for stories 100 or more days old, and '____???' for stories with no date (or invalid dates) in their RSS data.

The Starred indicator displays '*' for stories you have flagged as starred and nothing for stories which are unstarred. See Section 2.4 for more information on this.

The Read/Unread indicator displays '@' for unread stories and '-' for stories which you have already read. As you read stories, they will remain in place among the new stories until the next feed poll (see Section 3.3) occurs; they will then be sorted with the old stories.

The list cursor is displayed as a bolded reverse-video bar spanning the width of the pane.

  | Martian rover rolls free of trap    BBC News         00h 52m   @
  | Pacman comes to life virtually      BBC News         00h 38m * @ 
  | Many dead in Nepalese bus blast     BBC News         00h 12m   @
  | --------------------------------------------------------------
  | China dismisses US Tiananmen call   BBC News         03h 38m * -
  | Japanese firms raise investment     BBC News         04h 12m   -

When the cursor is moved from the currently selected story, that story is displayed in bold face:

  | Martian rover rolls free of trap    BBC News         00h 52m   @
  | Pacman comes to life virtually      BBC News         00h 38m * @ 
  | Many dead in Nepalese bus blast     BBC News         00h 12m   @
  | --------------------------------------------------------------
  | China dismisses US Tiananmen call   BBC News         03h 38m * -
  | Japanese firms raise investment     BBC News         04h 12m   -

The cursor will return to the top of the list when a feed poll occurs.

2.1.3 The Status Line

Between the List Pane and the Story Pane sits the Status Line. It is a single line which displays various and sundry informational mesasges for you. It cannot be turned off, but its behavior can be modified.

Most of the status line is taken up by the Staus Area. If enabled as per Section 3.2.1, the Status Area will display a message in the following format when no other activity is happening:

  |
  | [S: 1/175] [U/N: 21/21] [P: 5 @ 05:45 | 6 since 02:41] [F: F,S]     
  |

It is read as follows:

[S: n/n]
Story count. The first number is the number (in the story list) of the location of the list cursor. The second number is the total number of stories in the list.
[U/N: n/n]
Unread/New count. The first number is how many unread stories remain in the list. The second number is now many new stories are currently in the list.
[P: n @ HH:MM | n since HH:MM]
Polling data. The data on the left side of the pipe is how many feeds were refreshed during the last poll, and the time at which that poll occurred. The date on the right side is how many feeds total have been refreshed since the last user activity (presently defined as "reading a story").
[F: X,Y,..Z]
Filter status. The comma-separated list of letters indicated which filters are active. Current possible values are F for flagged feeds and S for starred stories. If no filters are active, this block will not display at all.

When polling is active, the Status Area displays brief messages about the polling process (Checking..., Fetching..., etc.) for each feed being polled. When the Story Pane is focused, the Status Area displays the title (the actual title, taken from the feed, not the nickname displayed in the story list) of the feed from which the currently selected story is taken.

The rightmost character of the Status Line is the Focus Pointer. It simply points to whichever of the the List Pane (^) or the Story Pane (V) is focused.

2.1.4 Story Pane

The bottom half of Olive's window is used by the Story Pane. This is where the content of (and some other information from) the currently selected story is displayed.

  |             Story Title
  |             -----------
  |
  | -- Header
  |
  | Lorem ipsum dolor sit amet, consectetuer 
  | adipiscing elit. Vivamus rhoncus libero. 
  |
  | Aenean sit amet risus sed felis molestie 
  | hendrerit. Pellentesque feugiat mauris.
  |
  | -- 
  | http://someurl/pathto/story.html

2.1.5 Searching

The List and Story panes, as well as the feed Edit and Remove panels, have capabilities much like those of the Unix utility less.

  • Press '/' to start a search, then...
  • Type your search term and press Enter, which will cause the cursor to jump to the first matching line, or display Not found if there is no match.
  • Optionally, press 'n' to repeat the search forward or 'N' to repeat it backwards,
  • Press Enter to select the current match and end the search or...
  • Press 'q' to end the search without selecting anything.

2.2 Reading The News

The selection cursor in the List Pane is moved one line at a time with the up and down arrow keys or the 'j' and 'k' keys, Pressing Enter selects the current story and causes its contents to be loaded into the Story Pane.

Cursor movement and story selection functions are combined in the '[' and ']' keys, which select the previous and next stories,respectively.

The cursor can also be moved one screen at a time with PgUp and PgDn, or to the top and bottom of the list with Home and End.

Individual stories can be marked or unmarked as read with the 'm' and 'u' keys. All stories can be marked/unmarked at once with 'M' and 'U'.

When the Story Pane is focused, the story text can be scrolled one line up or down with the arrow keys or the 'j' and 'k' keys. It can also be scrolled one screen at a time with PgUp and PgDn, or to the top and bottom with Home and End.

2.3 Link Handling

Nearly all stories will have a single link URL attached to them which leads to a fuller or permanent copy of the story from the feed. This URL is displayed after the story text, and can be followed by pressing the 'l' (ell) key.

Some news stories have URLs embedded in their content, in addition to the dedicated link URL. These can be accessed by pressing 'L', which will pop up a Linklist in the top-right corner of the Olive window.

    .[ Linklist ]-----------------------------------.
    |                                               |
    | [ 6] permalink                               ||
    | [ 7] incorporated                            ||
    | [ 8] entry                                   #|
    | [ 9] discovery                               #|
    | [10] Patrick Stewart                         #|
    | [11] Circulating intentional data            ||
    |                                               |
    |                                    < Cancel > |
    |                                               |
    '-----------------------------------------------'

The numbers correspond to markers which are placed in the story text, as in:

    On the test version of the catalog he has [incorporated][7]
    tagging, which he discusses in a recent [entry][8] on his blog.

Select a link with the Up/Down arrow keys and press Enter to launch an external browser session with the URL of that link as the target. If Global Story Paging has not been disabled, the story text can be paged while the Linklist is displayed. The linklist will be dismissed when you Tab to the "Cancel" button and press Enter.

2.4 Starred Stories and Flagged Feeds

Olive has a pair of selector mechanisms to help you separate the wheat from the chaff in your newsreading: starring stories and flagging feeds.

Starred stories will remain in your story list even after they have "fallen off" the feed they were sourced from. This lets you force stories to stick around if you need them for later reference. The star flag of a story is toggled on/off with the 's' key. Once unstarred, a story will be removed from the story database if it is no longer in the feed it came from. The secondary effect of starring stories is that you can filter the story list to show only the currently starred stories with the 'S' key.

Unlike starred stories, which are immortal, There is nothing special about flagged feeds except that you have "flagged" them as special in some way. For instance, I flag the feeds which belong to journals of my friends, marking them as distinct from the general news feeds I subscribe to. The story list can be filtered to show only stories from flagged feeds with the 'F' key. See Section 4.1 for information on flagging feeds.

The current filtering status will be shown in the Status Area of the Status Line when filtering is active.


(Getting Started) Prev | Index | Next (Setting Options)


$Id: news.html 368 2006-01-05 01:20:15Z mdxi $

olive-1.3/docs/start.html0000644000175000017500000001462710374523573014155 0ustar mdximdxi User Manual: Starting Out

1. Getting Started

Prev | Index | Next (Reading News)


1.1 Installation

Installing Olive is a simple, streamlined process (so long as you're not on OS X). After unpacking the tarball, cd into the resultant directory and run 'make'.

NB: If you are on a BSD system, run 'gmake' instead of 'make'.

This will run a dependancy check and report the results. You may see:

  mdxi@fornax:~/olive$ make
  Checking for dependancies...looks good.
  mdxi@fornax:~/olive$ 

but chances are that you'll see something more like this:

  mdxi@fornax:~/olive$ make
  Checking for dependancies...Required modules not found:
      Config::YAML
      DBD::SQLite
      XML::Simple
  make: *** [olive] Error 1
  mdxi@fornax:~/olive$ 

The easiest thing to do, should you be missing modules, is to let the cpan script install Bundle::Olive for you. This will install all of Olive's dependancies (and all of their dependancies), all at once. If, for whatever reason, you don't wish to do this, install the list of modules reported as missing by whatever mechanism you wish. The effect is the same.

With all deps installed, you may want to look at the top of the Makefile, where you'll find this:

  PREFIX  = /usr/local
  BINDIR  = $(PREFIX)/bin
  LIBDIR  = $(PREFIX)/share/olive
  DOCDIR  = $(PREFIX)/share/doc/olive

If this (very standard) layout isn't to your liking, modify it to suit you. Now it's time for the actual install.

  mdxi@fornax:~/olive$ sudo make install
  Checking for dependancies...looks good.
  Prepping for install.
  Creating install directories.
  Installing Olive.
  Cleaning up.
  Done. Now run /usr/local/bin/olive to get started!

That's all there is to it.

1.2 OPML Import

If you have been using NetNewsWire or Bloglines, Olive can import OPML files exported by them, sparing you from having to manually add those feeds. This would be a good time to do that.

Just run 'olive [OPML_FILE]' and the import process will proceed.

 mdxi@fomalhaut:~$ ./olive bloglines.opml 
 Olive starting up.
 Loading modules.
 Initialising objects and data.
 Beginning OPML import.
 Descending into HTML-CSS-DTHML-Javascript...
 Descending into News...
 Descending into Of Interest...
 Descending into People...
 Descending into Perl...
 Descending into User Groups...
 Descending into VIm...
 71 of 75 feeds imported
 See ~/.olive/errors.log for more information

 mdxi@fomalhaut:~$ cat ~/.olive/errors.log
 -- Starting up at 2006-00-04T19:47:15 --
 The nick 'The Daily Herald' is in use or reduces to a nick which is in use (http://www.harktheherald.com/feed_letters.php).
 The nick 'The Daily Herald' is in use or reduces to a nick which is in use (http://www.harktheherald.com/feed_top.php).
 The nick 'The Daily Herald' is in use or reduces to a nick which is in use (http://www.harktheherald.com/feed_opinion.php).
 The nick 'Jeremy Zawodny's' is in use or reduces to a nick which is in use (http://jeremy.zawodny.com/blog/rss2.xml).

 mdxi@fomalhaut:~$

This can be done at any time (but shouldn't be done when another copy of Olive is running).

As the example above shows, any feeds in the OPML file which are already found in the Olive feed list will be rejected and a notice will be written to the error log. Feeds whose descriptions cause collisions with existing feeds will be similarly rejected.

1.3 First Run

The first thing you'll see is a splash screen welcoming you to Olive and telling you how to get to the help popup. This will only be shown the first time you run Olive.

Once the welcome message is cleared, you'll be looking at the main Olive interface. If you have already imported a feed list, the feeds will be polled and processed and you'll be ready to go. If not, your screen will be mostly blank and you'll need to add some feeds. You can do this by pressing 'C-a' and filling in values in the dialogue which pops up. You will also want to set your external browser command in the Options panel. Press 'C-o' to invoke that panel and see Section 3.5 for more information.

The top half of the screen is occupied by the List Pane, where the list of available stories is displayed. Use the up and down arrow keys to navigate it and the Enter key to select a story. Once selected, the content of the story will be displayed in the Story Pane, at the bottom of the screen (if you need to scroll this pane, use the Spacebar to page the story text down and the '-' key to page it up).

To exit Olive, press 'Q'.

There are more advanced functionality and alternate keys available (that's what the rest of this manual is for) but those are the basics.

1.4 The Help System

As long as there isn't a dialog on screen, you can press 'h' or '?' to access the help popup, which contains a list of keybindings and some other information.

More complete help is found in this manual, which can be accessed from within Olive by pressing 'C-n'. This will launch an external browser with the manual index loaded.

1.5 Feedback and Bug Reports

You can view existing bug reports and file new ones at the Olive Trac wiki. If that doesn't work for you, or if you just have comments or questions, send them to me at <mdxi@cpan.org>


Prev | Index | Next (Reading News)


$Id: start.html 426 2006-02-15 04:00:59Z mdxi $

olive-1.3/docs/page.css0000644000175000017500000000112210250433634013531 0ustar mdximdxibody {background-color: white; font-family: sans-serif;} h1,h2,h3 { font-family: monospace; } h1 { border-bottom: solid #e0d9c0 medium; } blockquote { background-color: #e0d9c0; font-style: italic; padding: 1em; } pre { margin-left: 3.5em; padding: 1em; background-color: #000; color: #ccc; margin-right: 5.5em; } table { margin-left: 2.5em; margin-right: 4em; } th {background-color: #e0d9c0;} td {vertical-align: top; padding: 0.15em; border-right: solid #000 thin; border-bottom: solid #000 thin; background-color: #eee; } olive-1.3/docs/olive.css0000644000175000017500000000271510356360155013750 0ustar mdximdxibody { font-family: sans-serif; margin: 0 5% 0 5%; } div { padding: 0 1% 1% 1%; margin-bottom: 2%; } div#entries { margin-left: 11em; } div#sidebar { width: 12em; font-size: small; position: fixed; top: 5.4em; left: 5%; background-color: #e0e0e5; padding: 0; } div.msg { background-color: #e0e0e5; margin-top: 0; margin-bottom: 0;} p.side { margin: 0; font-size: large; padding: 2px; background-color: #e0d9c0;} p.head { text-align: right; background-color: #bbb; margin: 0; padding: 2px;} p { margin-top: 0; } blockquote { margin-left: 1em; margin-right: 13em; border: solid #aa9339 thin; } blockquote p { margin-top: 0; } pre { margin-left: 0; margin-right: 10em; background-color: #ccc; color: #226; border: solid #aa9339 thin; padding: 0.25em; font-family: monospace; } table { width: 100%; margin: 0; } .left { padding: 0; border: 0; background: transparent; float: left; width: 49%; clear: left; } .right { padding: 0; border: 0; background: transparent; float: right; width: 49%; clear: right; } .c { text-align: center; } .c2 { text-align: center; background-color: #bbb; border: 0; } h2 { background-color: #e0d9c0; } h2.head { margin-bottom: 0;} a:visited { color: #009; } dt { color: #04a; font-weight: bold; } dd { /* background-color: #ddd; border: solid #aa9339 thin; */ padding: 0.25em; margin-right: 10em; }olive-1.3/docs/index.html0000644000175000017500000000621510370730036014107 0ustar mdximdxi Olive Documentation

Olive User Guide

Olive Logo

1. Getting Started

  1. Installation
  2. OPML Import
  3. First Run
  4. The Help System
  5. Bug Reports

2. Reading News With Olive

  1. The User Interface
    1. The Titlebar
    2. List Pane
    3. The Status Line
    4. Story Pane
    5. Searching
  2. Reading The News
  3. Link Handling
  4. Starred Stories and Flagged Feeds

3. Setting Options

  1. The Options Panel
  2. Checklist Options
    1. 'Show story list status'
    2. 'Skip unread on prev/next'
    3. 'Global story paging'
    4. 'Hide titlebar'
    5. 'Confirm on exit'
  3. 'Poll wait'
  4. 'Fetch timeout'
  5. 'Link command'
  6. Saving Options

4. Feeds

  1. Adding Feeds
    1. Forced Feeds
  2. Editing Feeds
    1. Dormant Feeds
  3. Removing Feeds
  4. Polling Feeds

5. The Dotdir

  1. Config File
    1. Miscellaneous Configuration
    2. Feeds
    3. Customizing Keybindings
  2. Error Log
  3. Story Database
  4. Feeds Directory
olive-1.3/docs/dotdir.html0000644000175000017500000002525510562256763014307 0ustar mdximdxi User Manual: Dotdir

5. The Dotdir

(Feeds) Prev | Index | Next


None of the information in this section of the manual is needed to use and enjoy Olive.

Olive keeps its configuration, raw data, and working data in a directory named '~/.olive'. This section describes the contents of that directory.

5.1 The Config File

The configuration file is named 'olive.yaml'. As its extension indicates, it is a YAML-formatted file. If you are unfamiliar with YAML, you should read about it before manually editing this file.

5.1.1 Miscellaneous Configuration

coe
Mnemonic: Confirm On Exit
Type: Boolean
Effect: Controls whether Olive termination is immediate (0) or confirmed (1). See Section 3.2.5.
dst
Mnemonic: Don't Show Titlebar
Type: Boolean
Effect: Controls whether the first line of the Olive window is used as a titlebar (0) or to display live list data (1). See Section 3.2.4.
fr
Mnemonic: First Run
Type: Boolean
Effect: Originally marked whether Olive had been run before (1) or not (0). Now used to store the version number of Olive when last run, paving the way for automatic upgrade procedures. Still controls display of the first-run splash screen,
gsp
Mnemonic: Global Story Paging
Type: Boolean
Effect: Controls whether keybindings are enabled so as to allow paging of the currently displayed story no matter where focus is. 1 is "bind", 0 is "don't bind". See Section 3.2.4.
knf
Mnemonic: Keep New Forever
Type: Boolean
Effect: Controls whether stories are removed from the database as they fall off their feeds, or are removed only after the user has read them. 1 is "keep", 0 is "let fall off". See Section 3.2.3.
loglevel
Mnemonic: None
Type: Enum
Effect: Sets the minimum logging level for the error log. Default is "warning". Meaningful values for Olive are "warning", "info", and "debug" (in increasing order of verbosity). For a full list of levels, see the Log::Dispatch documentation.
pw
Mnemonic: Poll Wait
Type: Positive Integer
Effect: Sets the inital value, in minutes, of the delay between automatic poll feeds. See sections 3.3 and 4.4 for more info.
sls
Mnemonic: Show List Status
Type: Boolean
Effect: Controls whether the status line will display story list stats (1) or not (0). See Section 3.2.1.
snu
Mnemonic: Skip to Next Unread
Type: Boolean
Effect: Controls the behavior of the prev/next commands: always select the story adjacent to the current selection (0) or seek to the next unread story (1). See Section 3.2.2.
to
Mnemonic: Time-Out
Type: Positive Integer
Effect: Sets the initial value for the network timeout of the internal LWP::UserAgent object which fetches feeds. See Section 3.4.
www
Mnemonic: ...
Type: String
Effect: Contains the command string which is used to launch external browser sessions. See Section 3.5.

5.1.2 Feeds

All information about subscribed feeds is stored in a HOHOH named 'feeds'. The full format is as follows:

  feeds:
    a_feed:
      disp: Display Title From User
      dormant: 0
      feed: http://feed/url
      force: 0
      last: 1119248150
      title: Actual Title From RSS Feed
      ttl: 86400

Where the 'a_feed' stanza is repeated, with appropriate changes, for each subscribed feed. The meanings of the individual values is:

  1. disp: The feed's user-chosen display name; displayed in the List Pane next to stories belonging to this feed.
  2. dormant: Boolean marking the feed dormant or not.
  3. feed: URL of the actual feed.
  4. force: Boolean marking the feed as force-fetchable or not.
  5. last:Value of time() as of the feed's last fetching.
  6. title: Actual title of the feed, taken from the feed data.
  7. ttl: The feed's time-to-live in seconds. Taken from the feed or given a value of 3600s in the absence of a value from the feed data.

See Section 4 for more information.

5.1.3 Customizing Keybindings

There is no interface provided for changing keybindings within Olive. Not all functions can be rebound. The ones which can are altered via a HOH named 'keys' in the config file. An example which creates custom bindings for the functions normally assigned to the '[' and ']' keys is:

  keys:
    next: n
    prev: p

Here is the list of bindable functions and their standard keys:

FunctionKeyDescription
prev [ Go to previous story
next ] Go to next story
mark m Mark story read
unmark u Mark story unread (unmark story)
star s Toggle story starred/unstarred
markall M Mark all stories read
unmarkall U Mark all stories unread (unmark all)
gpdn ' ' (Space) Global story paging: pagedown
gpup - Global story paging: pageup
focus w Shift focus between list/story panes
link l Execute defined link command with story URL
poll p Poll for updated feeds
force P Force-poll feeds marked as forced
filterf F Filter story list to show only stories from flagged feeds
filters S Filter story list to show only starred stories

Olive does not check for duplicate bindings or stomping on a prebound key without providing a replacement. Also, be aware that you may need to quote your choice of key if it is a YAML special character (see the YAML spec for more information).

5.2 The Error Log

Olive logs all warnings and fatal error messages (trapped or otherwise) to a file named 'errors.log'. On startup, this file is copied to 'errors.log.1' before being overwritten, so there are logs for the past 2 runs available at any time.

Error logs always start with a timestamp like this:

  -- Starting up at 2005-05-22T16:58:11 --

After that, there is no standard or predictable format for the contents of the file.

Should Olive ever suddenly quit, or if you experience any problems or weird behavior, please look at the logfile(s) and file a bug (as per Section 1.5)

5.3 The Story Database

Olive does not operate upon the raw feeds it fetches over the network. It processes them after they are retrieved and stores the relevant data in a SQLite database named 'story.db'.

The schema of the database is as follows:

  CREATE TABLE stories (id INTEGER PRIMARY KEY, 
                        nick TEXT, 
                        timestamp INT,
                        md5 TEXT, 
                        read INT, 
                        new INT, 
                        link TEXT, 
                        title TEXT, 
                        desc TEXT);

5.4 The Feeds Directory

Downloaded feeds and the data needed to perform tests for HTTP 304 status are stored in a subdirectory called 'feeds'. Every feed you're subscribed to will have two files there, a file named as a possibly modified form of the feed nickname, and file with the same name as the first one, but with ".cache" appended.

  mdxi@fornax:~$ ls .olive/feeds/
  atc             cnn_money        groklaw        mdxi         rjbs        
  atc.cache       cnn_money.cache  groklaw.cache  mdxi.cache   rjbs.cache  
  bbc_news        gloria           kate           olive        robert      
  bbc_news.cache  gloria.cache     kate.cache     olive.cache  robert.cache

The files with no extension hold the last downloaded copy of that feed. The .cache files hold HTTP header data needed to check if the feed has been updated when it is polled next.

Files associated with a feed are unlinked when the feed is removed from your feed list.


(Feeds) Prev | Index | Next


$Id: dotdir.html 434 2007-02-07 05:04:51Z mdxi $

olive-1.3/docs/feeds.html0000644000175000017500000002236710370730036014074 0ustar mdximdxi User Manual: Feeds

4. Feeds

(Setting Options) Prev | Index | Next (Dotdir)


4.1 Adding Feeds

Feeds are added to Olive via the Add New Feed dialog, which is invoked by pressing 'C-a'.

  .-[ Add New Feed ]-----------------------------------------.
  |                                                          |
  | Location: [                                            ] |
  |                                                          |
  | Nickname: [                ]                             |
  |                                                          |
  |           [ ] Flag this feed                             |
  |                                                          |
  |           [ ] Force-poll this feed                       |
  |                                                          |
  |                                        < OK > < Cancel > |
  |                                                          |
  '----------------------------------------------------------'

Fill out the form and select OK to attempt to store the feed. It will be tested for network availablility, and if Olive can fetch the feed, it will be added. If the feed cannot be found or fetched, an error will be displayed and it will not be added to the feed list.

The Location field holds the URL of the feed.

Nickname is a short (16-character maximum) display name which will be used in the List Pane.

Toggling "Flag this feed" causes the feed to be flagged, as described in Section 2.4.

4.1.1 Enabling Force-poll

The "Force-poll this feed" option enables an on-demand override of normal feed polling procedure. It causes TTL checks to be skipped and treats a 304 Not Modified as a 200 OK, causing a fetch of the feed regardless of elapsed time or the actual modification status of that feed. This should only be used when there is a problem getting a feed which you know has changed, but which is not updating for whatever reason.

4.2 Editing Feeds

To change information about a feed, first invoke the Edit Feeds dialog by pressing 'C-e'. You will be presented with a list of feeds and their current statuses, ordered alphabetically by feed nickname.

  .-[ Edit Feeds ]-------------.
  |                            |
  |  Feed              Status  |
  |                            |
  |  ATC               f--   # |
  |  BBC News          ---   # |
  |  bda               f--   # |
  |  code4lib          --d   # |
  |  Dru               f--   # |
  |  Groklaw           ---   # |
  |  Kate              fp-   # |
  |  LtU               ---   | |
  |  OBSD Security     ---   | |
  |  Olive             -p-   | |
  |                            |
  |                    < OK >  |
  |                            |
  '----------------------------'

The three statuses are:

  • Flagged, indicated by an 'f' in the first status column.
  • Force-polled, indicated by a 'p' in the second status column.
  • Dormant, indicated by a 'd' in the third status column.

It is possible for a single feed to have multiple statuses set. Statuses not set display a hyphen.

Use the arrow keys to move through the list and select a feed with Enter. This will bring up the edit dialog for that feed.

  .-[ Edit BBC News ]----------------------------------------.
  |                                                          |
  | Location: [http://newsrss.bbc.co.uk/rss/newsonline_wor$] |
  |                                                          |
  | Nickname: [BBC News       ]                              |
  |                                                          |
  |           [ ] Flag this feed                             |
  |                                                          |
  |           [ ] Force-poll this feed                       |
  |                                                          |
  |           [ ] Feed is dormant                            |
  |                                                          |
  |                                        < OK > < Cancel > |
  |                                                          |
  '----------------------------------------------------------' 

Everything here works just as above, except for the addition of the last checkbox. Feeds are tested for network accessibility before changes are applied.

4.2.1 Dormant Feeds

Dormant feeds are never polled, even if they are also marked as being force-polled. If Olive sets a feed dormant due to errors, you can come here to unset that flag. The dormant flag will not be unset if Olive cannot fetch the feed.

4.3 Removing Feeds

To change information about a feed, first invoke the Remove Feeds dialog by pressing 'C-r'. You will be presented with a list of feeds and their current statuses, ordered alphabetically by feed nickname.

  .-[ Remove Feeds ]---------------.
  |                                |
  |  Sel Feed              Status  |
  |                                |
  |  [ ] ATC               f--   # |
  |  [ ] BBC News          ---   # |
  |  [ ] bda               f--   # |
  |  [ ] code4lib          --d   # |
  |  [ ] Dru               f--   # |
  |  [ ] Groklaw           ---   # |
  |  [ ] Kate              fp-   # |
  |  [ ] LtU               ---   | |
  |  [ ] OBSD Security     ---   | |
  |  [ ] Olive             -p-   | |
  |                                |
  |                        < OK >  |
  |                                |
  '--------------------------------'

Simply go through the list, using Enter to select feeds for deletion, then select 'OK' to remove those feeds. Their stories will be removed from the story database, their feed and cache files will be deleted from disk, and their information will be purged from Olive's feed list.

4.4 Polling Feeds

Feeds are polled (checked for updates) every time the period specified in the Poll Wait option elapses. You can request an early poll with the 'p' key. Feeds which are marked as forced can be fetched on demand via the 'P' key.

Here is the proceedure Olive follows for polling non-forced feeds.

  • Each feed's last-checked time is subtracted from the current time and the difference is compared to that feed's TTL (time-to-live) value.
  • If the time difference is less than the TTL, no further action is taken for that feed.
  • If the time difference is greater than or equal to the TTL, the feed is polled.
  • A polled feed is first pinged, to check its status.
  • If the ping (not a literal ICMP ping, but a HTTP HEAD request) comes back as a 304 Not Modified, no further action is taken for that feed.
  • If the ping indicates success, the feed is fetched and processed.
  • If the ping comes back as 403 Forbidden or 410 Gone, the feed will be marked dormant and you will be notified of this via a dialog box.
  • If the ping indicates some other error condition, information is logged to the error log and the TTL is modified (first dropped, then incrementally lengthened on successive errors).
  • If a feed has 5 successive failures, it will be marked dormant and you will be notified of this via a dialog box.

(Setting Options) Prev | Index | Next (Dotdir)


$Id: feeds.html 419 2006-02-03 19:33:50Z mdxi $

olive-1.3/docs/options.html0000644000175000017500000001371710562256763014515 0ustar mdximdxi User Manual: Options

3. Setting Options

(Reading News) Prev | Index | Next (Feeds)


Olive provides an in-app interface for setting most of the options which control its behavior. This is the Options Panel.

3.1 The Options Panel

The Options Panel is invoked with 'C-o', and it looks like this:

    .[Options]------------------------.
    |                                 |
    |  [X] Show story list status     |
    |  [ ] Skip unread on prev/next   |
    |  [ ] Keep stories until read    |
    |  [X] Global story paging        |
    |  [ ] Hide titlebar              |
    |  [ ] Confirm on exit            |
    |                                 |
    | [15] Poll wait (in minutes)     |
    | [30] Fetch timeout (in secs)    |
    |                                 |
    | [firefox -remo$] Link command   |
    |                                 |
    |                        < Save > |
    |                                 |
    '---------------------------------'

Use the Tab key to move focus between options.

3.2 Checklist Options

Use the up/down arrow keys to move between the items of the checklist and the Enter key to toggle items on or off.

3.2.1 'Show story list status'

This option controls whether the Status Line displays story count information when Olive is idle. If checked, it will enable a display as per Section 2.4.1. If unchecked, nothing will be displayed except the transient messages caused by various activities.

3.2.2 'Skip unread on prev/next'

This option controls whether previously-read stories will be skipped when the previous/next story keys ('[' and ']' by default) are used. If checked, Olive will seek to the next unread story (or the end of the list) in the given direction. If unchecked, prev/next always go to the previous and next stories from the current location in the list, regardless of read/unread state.

3.2.3 'Keep stories until read

By default Olive removes stories from the Story List when they "fall off" the feed they came from. If this option is checked, Olive keeps stories in the List until you have read them. Be aware that this can cause a lot of stories to pile up, depending on how many feeds you subscribe to and how often you read news.

3.2.4 'Global story paging

If this option is checked, then at the next restart keys will be bound to allow global paging (up/down) of the currently displayed story. That is, to allow paging the current story up or down regardless of which pane is focused. The default keys for this are ' ' (Space) for PgDn and '-' for PgUp, but these can be customized as per Section 5.1.3.

This option combined with the above option allow for newsreading with an absolute minimum of effort.

3.2.5 'Hide titlebar'

If this option is checked, the titlebar will not be displayed after the next restart. This gives you one more line of vertical real estate for holding actual data. If unchecked, the top line of the Olive window will be used as described in Section 2.1.

3.2.6 'Confirm on exit'

This option controls whether Olive instantly terminates when the quit command ('Q') is received. If checked, Olive will prompt you with a dialog to confirm exit. If unchecked, Olive will silently and immediately quit.

3.3 'Poll wait'

The value entered here must be a positive, nonzero integer.

This option sets the delay, in minutes, between polls of the feed list (see Section 4.4 for detailed information on polling). The default value is 15.

3.4 'Fetch timeout'

The value entered here must be a positive, nonzero integer.

This option sets the number of seconds which can pass before Olive gives up on contacting a server to fetch a feed. After this number of seconds, an error dialog will be displayed. The default value is 30.

3.5 'Link command'

This option sets the command which will be used to launch an external browser for viewing story links and/or the manual. The general format of this command is

    command_string LINK

Where 'LINK' is the literal string "LINK". This ("LINK") will be replaced with the value of the URL being passed to the browser, enclosed in singlequotes. Some suggestions:

  • w3m under screen: screen w3m LINK
  • Firefox under X11: firefox -remote 'openurl(LINK,new-tab)'
  • Default browser under OS X: open LINK

Do not use a console mode browser like w3m or links without being inside screen and having 'screen' prefacing your browser call (as above)!

3.6 Saving Options

Tab to the <Save> button and press Enter to finalize your changes. All changes take effect instantly except turning the titlebar on or off.


(Reading News) Prev | Index | Next (Feeds)


$Id: options.html 434 2007-02-07 05:04:51Z mdxi $ olive-1.3/README0000644000175000017500000000054410374523573012053 0ustar mdximdxi README for Olive Release 1 Patch 1 ---------------------------------- This is the first patch release to the Olive stable branch. There are no major changes in this release. See the CHANGELOG for more information on updates or INSTALL for information on the installation process. Bugs and feedback to olive-1.3/OliveWindow.pm0000644000175000017500000002050410626577122013774 0ustar mdximdxipackage OliveWindow; require Exporter; use warnings; use strict; our @ISA = qw(Exporter); our @EXPORT = qw( &wininit &winflip &winstatus &winstatmsg &winredraw ); ####################################################################### ## window and system logic ########################################## ####################################################################### sub linklist { my $cui = shift; my $d = $cui->userdata->{dbh}; my $sid = $cui->userdata->{sid}; my $w = $cui->userdata->{wins}; if($cui->getobj('linkl')) { $cui->getobj('linkl')->focus; $cui->getobj('linkl')->draw; return; } my ($count) = $d->selectrow_array("SELECT count(sid) FROM links WHERE sid = $sid"); return unless $count; my @links = (); my %descs = (); my $maxheight = int($w->{dim}[1] / 2); my $height = 0; my $width = 0; my $i = 0; my $statement = "SELECT link, desc FROM links WHERE sid = $sid ORDER BY num"; my $sth = $d->prepare($statement); $sth->execute; while (my $q = $sth->fetchrow_arrayref) { $links[$i] = $q->[0]; $q->[1] = substr($q->[1],0,32) . '...' if (length($q->[1]) >= 40); $q->[1] = sprintf("[%2d] %s",$i,$q->[1]); while (defined $descs{$links[$i]}) { # pad with spaces to make links unique (is this bad?) $links[$i] = join('',$links[$i],' '); } $descs{$links[$i]} = $q->[1]; my $len = length($q->[1]); $width = $len if ($len > $width); $i++; } $height = ($i < $maxheight - 6) ? ($i + 6) : $maxheight; my $winwidth = $width + 5; $winwidth = 15 if ($winwidth < 14); my $ll = $cui->add('linkl', 'Window', -border => 1, -bfg => 'blue', -height => $height, -width => $winwidth, -title => 'Linklist', -centered => 0, -y => 0, -x => $w->{dim}[0] - $winwidth + 1, ); $ll->{list} = $ll->add('llli', 'Listbox', -values => \@links, -labels => \%descs, -height => $height - 6, -width => $width + 2, -y => 1, -x => 1, -vscrollbar => 1, -onchange => sub { &OliveMisc::followlink($cui,$ll->{list}->get) }, ); $ll->{okb} = $ll->add('llok', 'Buttonbox', -y => -2, -x => $winwidth - 13, -buttons => [ { -label => '< Cancel >', -value => 1, -onpress => sub { $ll->loose_focus; $cui->enable_timer('clock'); $cui->delete('linkl'); $cui->draw }, } ], ); if ($cui->userdata->{c}->{gsp}) { $ll->set_binding( sub { $w->{story}{view}->cursor_pagedown; $w->{story}{view}->draw; }, $cui->userdata->{keys}->{gpdn} ); $ll->set_binding( sub { $w->{story}{view}->cursor_pageup; $w->{story}{view}->draw; }, $cui->userdata->{keys}->{gpup} ); } $cui->disable_timer('clock'); $ll->draw; $ll->focus; } sub winflip { my $cui = shift; my $c = $cui->userdata->{c}; my $focus = 0; if ($_[0] && ($_[0] eq 'story' or $_[0] eq 'news')) { $focus = $_[0]; } else { $focus = $cui->userdata->{focus}; } my $w = $cui->userdata->{wins}; my $msg = ''; if ($focus eq 'news') { $w->{news}{ftr2}->text('V'); $w->{news}{ftr2}->draw; $msg = $cui->userdata->{chan} if $cui->userdata->{chan}; $w->{news}{ftr1}->text($msg . ' ' x ($w->{dim}[0] - length($msg))); $w->{news}{ftr1}->draw; $w->{story}{view}->focus; $cui->userdata->{focus} = 'story'; } else { $w->{news}{ftr2}->text('^'); $w->{news}{ftr2}->draw; $msg = $cui->userdata->{stat} if ($cui->userdata->{stat} && $c->{sls}); $w->{news}{ftr1}->text($msg . ' ' x ($w->{dim}[0] - length($msg))); $w->{news}{ftr1}->draw; $w->{news}{list}->focus; $cui->userdata->{focus} = 'news'; } } sub wininit { my ($cui,$root) = @_; my $c = $cui->userdata->{c}; my %w = (); my $height = $root->height; my $width = $root->width - 1; my $r = $height % 2; my $title = "Olive " . $cui->userdata->{rel}; $w{dim}[0] = $width; $w{dim}[1] = $height; $w{dim}[2] = $r; $w{news} = $root->add('news', 'Window', -border => 0, -height => int($height / 2), ); $w{news}{head} = $w{news}->add('header', "Label", -bg => 'blue', -fg => 'white', -text => $title . (' ' x ($width - length($title) + 1)), -bold => 1, ) unless $c->{dst}; $w{news}{ftr1} = $w{news}->add('footer1', "Label", -bg => 'blue', -text => ' ' x $width, -width => $width, -y => int($height / 2) - 1, -bold => 1, ); $w{news}{ftr2} = $w{news}->add('footer2', "Label", -bg => 'blue', -bold => 1, -text => '^', -width => 1, -x => $width, -y => int($height / 2) - 1, ); $w{story} = $root->add( 'story', 'Window', -border => 0, -height => int($height / 2) + $r, -y => int($height / 2), ); $w{story}{view} = $w{story}->add('storyview', 'TextViewer', -wrapping => 1, -text => "", -vscrollbar => 1, ); return \%w; } sub winredraw { my ($cui,$root) = @_; my $sel = $cui->userdata->{wins}->{news}{list}->get_active_id; $cui->userdata->{nlsel} = $sel || '0E0'; $root->delete('news'); $root->delete('story'); $cui->layout; $cui->userdata->{wins} = wininit($cui,$root); &OliveFeed::refreshlist($cui); &OliveStory::storypick($cui); $cui->userdata->{nlsel} = 0; } sub winstatus { my $cui = shift; return if $cui->userdata->{shift}; my $w = $cui->userdata->{wins}; my $msg = ''; my $cur = $w->{news}{list}->get_active_id + 1; my $max = $w->{news}{list}->{-max_selected} + 1; $cur = ($cur < 1) ? 1 : $cur; # keep PgUp/PgDn from making $cur less than one $cur = ($cur > $max) ? $max : $cur; # or more than the max possible story my $ur = $cui->userdata->{ur}; my $new = $cui->userdata->{new}; $msg = "[S: $cur/$max] [U/N: $ur/$new] ".$cui->userdata->{poll}; if ($cui->userdata->{star} || $cui->userdata->{flag}) { $msg .= ' [F: '; $msg .= 'F' if $cui->userdata->{flag}; $msg .= ',' if ($cui->userdata->{flag} && $cui->userdata->{star}); $msg .= 'S' if $cui->userdata->{star}; $msg .= ']'; } $cui->userdata->{stat} = $msg; $w->{news}{ftr1}->text(join('',$msg,(' ' x ($w->{dim}[0] - length($msg))))); $w->{news}{ftr1}->draw; } sub winstatmsg { my ($cui,$msg) = @_; my $w = $cui->userdata->{wins}; $w->{news}{ftr1}->text(join('',$msg,(' ' x ($w->{dim}[0] - length($msg))))); $w->{news}{ftr1}->draw; } 1; =head1 COPYRIGHT & LICENSE Copyright 2005,2006 Shawn Boyette, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut olive-1.3/OliveOPML.pm0000644000175000017500000000730410357066343013276 0ustar mdximdxipackage OliveOPML; =head1 NAME OliveOPML - Badly import OPML files =head1 DESCRIPTION This library very stupidly attempts to read OPML files. At the moment it handles Bloglines and NetNewsWire output. If you want others added, send patches or an exported OPML file. =cut require Exporter; use warnings; use strict; use Config::YAML; use DBI; use Digest::MD5 qw( md5_hex ); use XML::Simple; our @ISA = qw(Exporter); our @EXPORT = qw( &opmlimport ); my $c = Config::YAML->new( config => "$ENV{HOME}/.olive/olive.yaml" ); my $i = my $j = 0; my $type = ''; sub opmlimport { my $opml = shift; my $feeds; my $x = XML::Simple->new; my $f = $x->XMLin($opml); print "Beginning OPML import.\n"; if (ref $f->{body}{outline} eq "ARRAY") { $feeds = $f->{body}{outline}; $type = 'nnw'; } elsif ( ref $f->{body}{outline} eq "HASH") { $feeds = $f->{body}{outline}{outline}; $type = 'bl'; } else { die "Can't find the freakin' feeds list!\n"; } if ($type eq 'nnw') { map { extract($_) } (@{$feeds}); } elsif ($type eq 'bl') { map { extract_bl($_) } (@{$feeds}); } print "$j of $i feeds imported\n"; print "See ~/.olive/errors.log for more information\n" if ($i != $j); } sub extract { my $outline = shift; my $text = my $url = 0; if ($outline->{text}) { $text = $outline->{text}; } elsif ($outline->{title}) { $text = $outline->{title} } else { 1; } print "$text\n"; if ($outline->{xmlUrl}) { $url = $outline->{xmlurl}; } elsif ($outline->{url}) { $url = $outline->{url}; } # throw back "container" outlines with no feed attached return unless ($text && $url); storefeed($text,$url); } sub extract_bl { my $outline = shift; my $text = my $url = 0; $text = $outline->{title}; if ($outline->{xmlUrl}) { $url = $outline->{xmlUrl}; } else { if (ref $outline->{outline} eq 'HASH') { extract_bl($outline->{outline}); } else { print "Descending into $text...\n"; map { extract_bl($_) } (@{$outline->{outline}}); } } # throw back "container" outlines with no feed attached return unless ($text && $url); storefeed($text,$url); } sub storefeed { my ($disp,$feed) = @_; $i++; # trim whitespace $feed =~ s/^\s+//; $feed =~ s/\s+$//; $disp =~ s/^\s+//; $disp =~ s/\s+$//; $disp = substr($disp,0,16); # save original nick for display and sanitize my $nick = $disp; $nick =~ s/\s/_/g; $nick =~ s/\W//g; $nick =~ s/_+/_/g; $nick =~ s/_$//; $nick = md5_hex($disp) if ($nick eq ''); $nick = lc($nick); # check for dupe nicks while (defined $c->{feeds}{$nick}) { print STDERR "The nick '$disp' is in use or reduces to a nick which is in use ($feed).\n"; return; } # check for dupe feeds foreach my $f (keys %{$c->{feeds}}) { if ( ($f ne $nick) && ($feed eq $c->{feeds}{$f}{feed}) ) { my $disp = $c->{feeds}{$f}{disp}; print STDERR "You're already subscribed to $feed as '$disp'\n"; return; } } # everything looks okay. store it. $c->{feeds}{$nick}{feed} = $feed; $c->{feeds}{$nick}{disp} = $disp; $c->{feeds}{$nick}{force} = 0; $c->{feeds}{$nick}{dormant} = 0; $c->{feeds}{$nick}{last} = 0; $c->{feeds}{$nick}{ttl} = 0; $c->write; $j++; } =head1 COPYRIGHT & LICENSE Copyright 2005,2006 Shawn Boyette, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut 1; olive-1.3/CHANGELOG0000644000175000017500000003041710562256770012410 0ustar mdximdxiChanges for r1p1 -> r1p2 ----------------------------------------------------------------------- * "Keep stories until read" option added * URL in help panel fixed Changes for r1 -> r1p1 ----------------------------------------------------------------------- * Fix for story selection problem after (un)starring a story * All files now uniformly licensed under Perl license * -h/--help and -v/--version options added * Startup messages removed ======================================================================= ======================================================================= Changes for b13 -> r1 ----------------------------------------------------------------------- * If a story was selected, then starred, then the link invoked, the link call would fail. Fixed * XML parsing crash condition was accidentally re-introduced by changed related to logging code in b11. Repaired * There are now 3 valid loglevels: debug, info, warning Changes for b12 -> b13 ----------------------------------------------------------------------- * Manual now complete (chapter 4 finally added) * Unicode handling fixes for List pane * Minor logging tweaks Changes for b11 -> b12 ----------------------------------------------------------------------- * New dependancy: Log::Dispatch::File, and thus a new Bundle::Olive. Note that you may have to delete /path/to/.cpan/Bundle/Olive.pm in order for 'cpan Bundle::Olive' to pick up the new version * New debug mode and more improvements to logging * OPML import fixes * Linklist would appear to have broken numbering when a URL was linked to more than once in a story. Fixed * Stories with no title no longer cause uninitialized value warnings * UTF-8 handling improvements * Regex for substituting 'LINK' with a URL in the external browser command is now global instead of single-instance * Database access optimizations and story rendering optimizations for better user experience * Local timezone correction was happening backwards. Fixed * Makefile now includes DESTDIR for package support * HEAD requests are now sent instead of GETs for pinging feeds Changes for b10 -> b11 ----------------------------------------------------------------------- * The "Focus story on select" option has been removed. "Global story paging" (from b9) and the new linklist capability make it thoroughly obsolete * All links within a story are now accessible via a popup linklist menu (default key is 'L') * Complete rewrite of HTTP code -- response codes 200, 300, 301, 302, 304, 307, 404, 4xx, 500, and 5xx are now properly handled. Failure codes are now handled smartly by Olive itself instead of just whining at the user (see http://trac.collapsar.net/olive/trac.cgi/wiki/OliveHttpHandling) * U/N counts now change as filters turn on and off * Marking a feed as dormant now skips all network activity related to a feed edit. Without this it was impossible to make an unreachable feed dormant * SQLite tweaks for higher performance while processing feeds (massive database changes) * HTML munging improvements -- lists are now given a basic treatment * Release as well as revision are now shown in the About Box * Titlebar length became off-by-one in b10. Fixed * The beginnings of smart upgrade handling are in place. Olive now complains if it detects you last ran an old version and tells you to delete your story database if so. * Hard feature freeze set for b12 -- no more new unplanned functionality from here on out * Large overhaul of the manual; Chapter 2 in particular is much more readable and informative. * POD effort begun, re-licensing begun, misc other crap Changes for b9 -> b10 ----------------------------------------------------------------------- * Add feed was broken in b9 due to an inadvertly copied line of code. b10 is an emergency release to fix this. * Last long-standing bug squashed: first story now selects properly at startup and after polls * List height tweak in Remove Feeds dialog * Trying to filter on flagged feeds when there are none is now a NOOP with an alert dialog like the one that accompanies attempting to force-poll feeds when none are marked as forced. * Ditto for starred stories Changes for b8 -> b9 ----------------------------------------------------------------------- * The old bug which caused some feeds to be polled whether their TTL had expired or not has been resolved * The story pane can now be paged up/down while focus is on the list pane (and the keys for this are configurable) * The default keys for poll/force-poll have changed from r/R to p/P * Stories can now be "starred", which results in them being immune from deletion until unstarred * Feeds can be now be marked as "flagged" * The story list can now be filtered to show only starred stories and/or flagged feeds * The Edit and Remove Feed dialogues are now sorted by feed name and display feed status info (flagged, forced, dormant) * Feeds from some sources (notably UseModWiki) would sometimes have blank descriptions which would become hashrefs after processing by XML::Simple, causing them to show up as changed every time they were parsed. Fixed, thanks to patch from Peter Mauriusz * Feeds with no items would cause a crashing bug in XML processing. Fixed * A different XML processing error would cause a crashing bug in OliveFeed. Fixed * More robust RFC822 date handling * Anonymous read-only repository access established Changes for b7 -> b8 ----------------------------------------------------------------------- * OPML import * Most common functions now have user-modifiable keybindings (see manual) and the help popup reflects real key mappings * Story list wasn't updating after some feed list updates. Fixed * XML::Parser errors are no longer fatal. They are now trapped and written to the error log. A dialog identifying the problematic feed is then displayed and the feed is marked dormant * Sometimes-inappropriate story URL extraction regex has been fixed * Removing a feed now removes its associated disk files * "Smart" HTML quotes now replaced with ASCII equivalents * Other HTML reworking improvements * Feeds with TTLs Olive considers invalid (less than 1s or non-numeric (except RDF legal values like "hourly") are now forced to one hour * Manual coverage is nearly complete. * Full source documentation and cleanup effort underway * Olive now has a Trac install for bug reporting and tracking at http://trac.collapsar.net/olive/trac.cgi/wiki Changes for b6 -> b7 ----------------------------------------------------------------------- * b6 fix for newlines broke them in a different way. Fixed * Story list wasn't updating on forced refreshes. Fixed * Add New Feed dialog was being destroyed on error. Fixed * Keybinding changes: mark/unmark is now 'm' and 'u' markall/unmarkall is now 'M' and 'U' * Olive now waits 30 seconds after the user's last keystroke before initiating a feed poll, should one come due while the user is active (NOTE: this only happens with a patched Curses::UI, as per INSTALL doc) * URL display in stories changed from the URL in brackets to [[url: URL ]] (and matching [[img: URL ]]) to avoid breaking putty URL-detection * Some feeds out there are RFC822-broken in a heretofore unseen way: no leading zero on the day-of-month. This is now checked for and worked around * errors.log is no longer lost on startup, it is copied to errors.log.1 and then a new logfile opened. A startup timestamp is also written to the log * Enhancements to HTML stripping and formatting routine * The 'About' box is now a window instead of a dialog, and hence is scrollable, and hence is where all the acknowledgements and credits are going from now on * The timeout for fetching feeds is now user-adjustable (default is old static value of 30s) * Bundle::Olive created and uploaded to the CPAN to handle deps * Olive now prints startup messages to STDOUT for the benefit of users on slower machines * Olive can now detect if its stor1y database has been deleted and self-repair * The (very in-progress) manual is now included and can be launched in the defined external browser with C-n Changes for b5 -> b6 ----------------------------------------------------------------------- * Titlebar can now be hidden, gaining an extra line of real estate * Newlines in entries were being converted to ''. Now they're converted to ' '. * ...and multiple strings of whitespace is being converted to a single space * Deps checking moved to external script called by Makefile; now reports all missing deps instead of just the first one encountered. * Licensing has changed from Revised BSD to MIT/X11. They're legally the same license, but the X11 doesn't have the "which version?" problem. Changes for b4 -> b5 ----------------------------------------------------------------------- * HTML now stripped from story titles * Recurring error dialog crashing bug fixed * Feed polling now disabled while dilogs are open Changes for b3 -> b4 ----------------------------------------------------------------------- * Installation now via Makefile (see INSTALL) * Feed properties editing implemented * Marking feeds as forced now actually enables a forced refresh. * Single stories are now mark/unmarkable as read. This is now bound to m/M; mark/unmark all has been moved to ,/< * Feeds of unrecognized types are now marked as dormant and will be ignored until that status is unset via the feed edit panel * Status line bolded for better visual separation * Revision number yanked from titlebar, moved to (Non-Secret) About Box * Screen initialization and redraw tweaks * URL dispatching bug fixed * STDERR now logged to ~/.olive/errors.log Changes for b2 -> b3 ----------------------------------------------------------------------- * >>> SEE README FOR IMPORTANT NOTICES * Sort options are gone. There is once again just the One True Sort Method, but this time it's way better than the last one. * Olive now sends conditional requests for feeds before fetching them and only does the actual fetch if the server reports that the feed has been changed since last fetch (IOW, it checks for HTTP 304 response codes -- other codes will be handled in the future). * Feed removal interface added * Story list status line now survives focus-switching * Story list status line presentation significantly overhauled * New config option: "Link command" * New function: open the selected story's link using the link command * Several keybindings changed * Help box is now scrollable * Story db is now vacuumed on every startup (keeps size down and speed up) * I _believe_ the dialog crashing bug has been dealt with * Optimizations in display and list-handling code Changes for b1 -> b2 ----------------------------------------------------------------------- * Network error dialog said "There was a problem problem..."; repeat fixed. * Null stories and titles are now caught and handled properly. * Olive would ABEND when a feed contained only a single . Fixed. * HTML header tags are now text-ified. * Items in the story list and the story title (when it occured) were being forced to UTF-8 all the time. This fixed a formatting offset bug under uxterm, but caused another when NOT under uxterm. Now this only happens when $ENV{LC_ALL} matches /UTF-8/. * '[' and ']' keys now work fine when Unread On Top is enabled and ENTER works as expected as well * Previously, both stories older than 99 days and stories with timestamps of epoch (i.e. with no time given in their feed) showed up as "+100d". Now genuinely old stories are "+99d" and stories with time epoch are "???". * Previously, if XML::SAX was installed, Olive's internal XML parser object would use that over the listed prerequisite of XML::Parser. However, XML::SAX is (of neccessity) far more heavyweight and slower than XML::Parser due to many features Olive does not need. XML::Parser is now used all the time. * The status message associated with feed polling (i.e. "N feeds updated at HH:MM") was dropping leading zeroes from the time. Fixed. * Dropped the timeout on fetching things over the network from LWP::UserAgent's defaut of 180s to 30s. * Leading and trailing spaces are now trimmed from story links and titles before insertion into the db * The story list can now be sorted oldest-to-newest olive-1.3/OliveStory.pm0000644000175000017500000003204710374513663013652 0ustar mdximdxipackage OliveStory; require Exporter; use warnings; use strict; use Date::Calc qw(Day_of_Week Mktime Timezone Delta_DHMS); use Encode; use OliveMisc; use OliveWindow; our @ISA = qw(Exporter); our @EXPORT = qw( &markall &markone &refreshlist &storyprev &storynext &shiftlist ); ####################################################################### ## story display logic ############################################## ####################################################################### sub markall { # sets the 'read' status of all db entries to true or false # depending on its second argument my ($cui,$ru) = @_; my $ff = ($ru == 1) ? 0 : 1; my $d = $cui->userdata->{dbh}; db_fastmode($d); $d->do("UPDATE stories SET read = $ru WHERE id > 0 AND read = $ff"); refreshlist($cui); db_safemode($d); } sub markone { # sets the 'read' status of current selected story to true or false # depending on its second argument my ($cui,$ru) = @_; my $w = $cui->userdata->{wins}; my $ff = ($ru == 1) ? 0 : 1; my ($sel) = $w->{news}{list}->get_active_id; $cui->userdata->{nlsel} = $sel; $cui->userdata->{nlsel} = $sel + 1 if $ru; # bump ahead one if marking 'read' $sel = $w->{news}{list}{-values}[$sel]; my $d = $cui->userdata->{dbh}; db_fastmode($d); $d->do("UPDATE stories SET read = $ru WHERE id = $sel AND read = $ff"); refreshlist($cui); shiftlist($cui); $w->{news}{list}->set_selection(-10061); db_safemode($d); } sub starone { my $cui = shift; my $w = $cui->userdata->{wins}; my $c = $cui->userdata->{c}; my $d = $cui->userdata->{dbh}; my ($sel) = $w->{news}{list}->get_active_id; $cui->userdata->{nlsel} = $cui->userdata->{star} ? 0 : $sel; $sel = $w->{news}{list}{-values}[$sel]; db_fastmode($d); my ($star) = $d->selectrow_array("SELECT star FROM stories WHERE id = $sel"); $star = $star ? 0 : 1; my $statement = "UPDATE stories SET star = $star WHERE id = $sel"; my $sth = $d->prepare($statement); $sth->execute; refreshlist($cui); shiftlist($cui); $w->{news}{list}->set_selection(-10061); db_safemode($d); } #------------------------------------------------------------- sub refreshlist { # (re)generates the olive story list my $cui = shift; my $w = $cui->userdata->{wins}; my $c = $cui->userdata->{c}; my $d = $cui->userdata->{dbh}; my $u = $cui->userdata->{utf8}; my $x = $w->{dim}[0]; my @values = (); my %labels = (); my $select = -1; my $fill = $x - 28; # how much space to reserve on the right my $flagged= ''; # sql subquery of flagged feeds my $r1 = 1; # have we already added the new/old separator? my @t = gmtime(( time() - ($cui->userdata->{tz} * 3600))); my $i = 0; my $j = 0; # set y-pos of list depending on whether we have a titlebar or not my $y = $c->{dst} ? 0 : 1; # build list of flagged feeds if we're filtering on that if ($cui->userdata->{flag}) { my $flagcount = 0; foreach my $nick (keys %{$c->{feeds}}) { next unless $c->{feeds}{$nick}{flag}; $flagged .= 'OR ' if $flagcount; $flagged .= "nick = '$nick' "; $flagcount++; } unless ($flagcount) { $cui->userdata->{flag} = 0; errorbox($cui,'There are no flagged feeds.','Info'); } } # and get a count of starred stories if filtering there if ($cui->userdata->{star}) { my $s = 'SELECT count(id) FROM STORIES WHERE star = 1 '; $s .= "AND ($flagged) " if $flagged; my ($starcount) = $d->selectrow_array($s); unless ($starcount) { $cui->userdata->{star} = 0; errorbox($cui, 'There are no starred stories in the set of flagged feeds.','Info') if $flagged; errorbox($cui, 'There are no starred stories.','Info') unless $flagged; } } # get counts of unread and new stories my $urstatement = 'SELECT count(id) FROM stories WHERE read = 0 '; $urstatement .= 'AND star = 1 ' if $cui->userdata->{star}; $urstatement .= "AND ($flagged) " if $flagged; my $newstatement = 'SELECT count(id) FROM stories WHERE new = 1 '; $newstatement .= 'AND star = 1 ' if $cui->userdata->{star}; $newstatement .= "AND ($flagged) " if $flagged; $cui->userdata->{ur} = $d->selectrow_array($urstatement); $cui->userdata->{new} = $d->selectrow_array($newstatement); # set up statements for gathering stories my @statement = (); $statement[0] = 'SELECT id,title,nick,timestamp,read,star FROM stories WHERE (new = 1 OR read = 0) '; $statement[0] .= 'AND star = 1 ' if $cui->userdata->{star}; $statement[0] .= "AND ($flagged) " if $flagged; $statement[0] .= 'ORDER BY timestamp'; $statement[1] = 'SELECT id,title,nick,timestamp,read,star FROM stories WHERE (new = 0 AND read = 1) '; $statement[1] .= 'AND star = 1 ' if $cui->userdata->{star}; $statement[1] .= "AND ($flagged) " if $flagged; $statement[1] .= 'ORDER BY timestamp DESC'; # fetch, and build list for (0..1) { my $sth = $d->prepare($statement[$_]); $sth->execute; while (my $q = $sth->fetchrow_arrayref) { # insert a row of '-'s between unread and read stories if ($_ == 1 && $i && $j && $r1) { $values[$i] = -10061; # NEWTON OS IS BETTER THAN YOU $labels{-10061} = '-' x $x; $i++; $r1 = 0; } my @t2 = gmtime($q->[3]); my $timestring = ''; if ($q->[3] == 0) { # no time given in feed $timestring = " ???"; } else { my ($Dd,$Dh,$Dm,$Ds) = Delta_DHMS(($t2[5]+1900),($t2[4]+1),$t2[3], $t2[2],$t2[1],$t2[0], ($t[5]+1900), ($t[4]+1), $t[3], $t[2], $t[1], $t[0]); $timestring = sprintf("%02dh %02dm",$Dh,abs($Dm)); $timestring = sprintf("%02dd %02dh",$Dd,abs($Dh)) if $Dd; $timestring = " +99d" if ($Dd > 99); } my $nick = sprintf("%-16s ",$c->{feeds}{$q->[2]}{disp}); $q->[1] = "[Null]" unless (defined $q->[1]); my $label = $q->[1]; Encode::_utf8_on($label) if ($u); $label = substr($label,0,$fill - 1); $label = $label . (' ' x ($fill - length($label))); my $read = $q->[4] ? ' -' : ' @'; # read/unread indicator my $star = $q->[5] ? ' *' : ' '; # starred indicator $values[$i] = $q->[0]; $labels{$q->[0]} = $label . $nick . $timestring . $star . $read; $i++; $j = 1 if ($_ == 0 && $i); } } $select = $cui->userdata->{nlsel} if $cui->userdata->{nlsel}; $w->{news}->delete('newslist') if (defined $w->{news}{list}); $w->{news}{list} = $w->{news}->add('newslist', 'Listbox', -y => $y, -values => \@values, -labels => \%labels, -height => int($w->{dim}[1] / 2) - (1 + $y), -selected => $select, -vscrollbar => 1, -onchange => sub { storypick($cui) }, -onselchange=> sub { winstatus($cui) if ($c->{sls} && !$cui->userdata->{shift})}, ); winflip($cui,'story') unless ($cui->userdata->{focus} eq 'story'); winstatus($cui) if $c->{sls}; } #------------------------------------------------------------- sub shiftlist { my $cui = shift; my $w = $cui->userdata->{wins}; $cui->userdata->{nlsel} = 0; $cui->userdata->{shift} = 1; my $halfheight = int($w->{news}{list}->canvasheight /2); my $i = my $j = 0; for $j (1..$halfheight - 1) { last if ($w->{news}{list}->{-ypos} == $w->{news}{list}->{-max_selected}); $w->{news}{list}->option_next; $i++; } for (1..$i) { $w->{news}{list}->option_prev; } $i = 0; for $j (1..$halfheight) { last if ($w->{news}{list}->{-ypos} == 0); $w->{news}{list}->option_prev; $i++; } for (1..$i) { $w->{news}{list}->option_next; } $w->{news}{list}->draw; $cui->userdata->{shift} = 0; winstatus($cui) if $cui->userdata->{c}->{sls} } #------------------------------------------------------------- sub storynext { my $cui = shift; my $w = $cui->userdata->{wins}; my $c = $cui->userdata->{c}; $cui->userdata->{shift} = 1; $w->{news}{list}->option_next; my $id = $w->{news}{list}->get_active_id; # handle skip-read if ($c->{snu}) { my $d = $cui->userdata->{dbh}; my $rid = $w->{news}{list}{-values}[$id]; my ($read) = $d->selectrow_array("SELECT read FROM stories WHERE id = $rid"); my $lastid = -1; while ($read) { $w->{news}{list}->option_next; $rid = $w->{news}{list}{-values}[$w->{news}{list}->get_active_id]; ($read) = $d->selectrow_array("SELECT read FROM stories WHERE id = $rid"); last if ($lastid == $rid); # infinite loop escape $lastid = $rid; } $id = $w->{news}{list}->get_active_id; } # make sure we're not standing on the divider storynext($cui) if ( $w->{news}{list}{-values}[$id] == -10061); # set new selection $cui->userdata->{shift} = 0; $w->{news}{list}->set_selection($id); } #------------------------------------------------------------- sub storyprev { my $cui = shift; my $w = $cui->userdata->{wins}; my $c = $cui->userdata->{c}; $cui->userdata->{shift} = 1; $w->{news}{list}->option_prev; my $id = $w->{news}{list}->get_active_id; # handle skip-read if ($c->{snu}) { my $d = $cui->userdata->{dbh}; my $rid = $w->{news}{list}{-values}[$id]; my ($read) = $d->selectrow_array("SELECT read FROM stories WHERE id = $rid"); my $lastid = -1; while ($read) { $w->{news}{list}->option_prev; $rid = $w->{news}{list}{-values}[$w->{news}{list}->get_active_id]; ($read) = $d->selectrow_array("SELECT read FROM stories WHERE id = $rid"); last if ($lastid == $rid); # infinite loop escape $lastid = $rid; } $id = $w->{news}{list}->get_active_id; } # make sure we're not standing on the divider storyprev($cui) if ( $w->{news}{list}{-values}[$id] == -10061); # set new selection $cui->userdata->{shift} = 0; $w->{news}{list}->set_selection($id); } #------------------------------------------------------------- sub storypick { my $cui = shift; my $w = $cui->userdata->{wins}; my $c = $cui->userdata->{c}; my $d = $cui->userdata->{dbh}; my $u = $cui->userdata->{utf8}; my $sel = $w->{news}{list}->get; my $id = $w->{news}{list}->get_active_id; return if (!defined $sel or $sel == -10061 or $sel eq ''); # hitting enter on the separator/with no feeds $w->{news}{ftr1}->text('Fetching...' . ' ' x ($w->{dim}[0] - 11)); $w->{news}{ftr1}->draw; db_fastmode($d); my @story = $d->selectrow_array("SELECT nick,title,timestamp,desc,link,read FROM stories WHERE id = $sel"); $cui->userdata->{sid} = $sel; $cui->userdata->{chan} = $c->{feeds}{$story[0]}{title}; $w->{news}{ftr1}->text(' ' x $w->{dim}[0]); winstatus($cui) if $c->{sls}; $w->{news}{ftr1}->draw; # just in case (especially titles) $story[1] = "[NULL]" unless (defined $story[1]); $story[3] = "[NULL]" unless (defined $story[3]); # paint story if ($u) { Encode::_utf8_on($story[1]); Encode::_utf8_on($story[3]); Encode::_utf8_on($story[4]); } my $pad = ' ' x int(($w->{dim}[0] - length($story[1])) / 2); my $pad2 = ' ' x ( int(($w->{dim}[0] - length($story[1])) / 2) - 2 ); my $title = join('',$pad,$story[1],$pad2,"\n",$pad,('-' x length($story[1])),$pad2,"\n\n"); $story[3] = join('',$title,$story[3],"\n\n--\n",$story[4]); $w->{story}{view}->text($story[3]); $w->{story}{view}->cursor_to_home; $w->{story}{view}->draw; # there's been user action; reset the "polled since" hash undef $cui->userdata->{polled}; undef $cui->userdata->{pwhen}; # mark story read unless ($story[5]) { # if unread... $d->do("UPDATE stories SET read = 1 WHERE id = $sel"); # sekrit message passing for menu rebuild $cui->userdata->{nlsel} = $id || '0E0'; refreshlist($cui); } # window+system housekeeping shiftlist($cui); db_safemode($d); } =head1 COPYRIGHT & LICENSE Copyright 2005 Shawn Boyette, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut 1; olive-1.3/depchecks0000644000175000017500000000103610252673036013037 0ustar mdximdxi@deps = @ARGV; foreach $dep (@deps) { $odep = $dep; $dep =~ s|::|/|g; $dep .= ".pm"; $i = 0; foreach $path (@INC) { if (-e "$path/$dep") { $i = 1; last; } } unless ($i) { $j++; $baddeps .= " $odep\n"; } } if ($j) { print "Required modules not found:\n"; print $baddeps; print "You can either install them manually or do:\n"; print " cpan Bundle::Olive\n"; print "to slurp them all in at once.\n"; exit 1; } olive-1.3/OliveHTTP.pm0000644000175000017500000002773210615365155013315 0ustar mdximdxipackage OliveHTTP; =head1 NAME OliveHTTP - Olive's HTTP Engine =head1 DESCRIPTION This library handles most network activity for Olive, and will eventually handle it all. The whole thing is just a wrapper around L. =cut require Exporter; use warnings; use strict; use HTTP::Request; use OliveMisc; use OliveWindow; our @ISA = qw(Exporter); our @EXPORT = qw( &fetchfeed ); =head1 Exported Subroutines =head2 fetchfeed =cut sub fetchfeed { my ($cui,$nick) = @_; my $c = $cui->userdata->{c}; my $l = $cui->userdata->{log}; my $ua = $cui->userdata->{ua}; my $cachefile = $cui->userdata->{feeds} . "/$nick.cache"; my $redir_count = 0; my $handled = 0; my $res; # load cached feed status my $msg = join(' ', 'Checking', $c->{feeds}{$nick}{disp}, 'status...'); winstatmsg($cui,$msg); my $cache = load_feed_cache($cui,$cachefile,$nick); until ($handled or $redir_count == 10) { # check current feed status $l->log( level => "debug", message => "$nick: Pinging $c->{feeds}{$nick}{feed}" ); $res = ping_feed($cui,$nick, $cache->{If_None_Match},$cache->{If_Last_Modified}); # and handle response $handled = handle_feed($res,$cui,$c,$nick); $redir_count++; } # if we hit the redirection limit, count it as a failure if ($redir_count == 5) { $l->log( level => "warning", message => "$nick: REDIRECT LIMIT EXCEEDED"); failure_handler($cui,$nick); } return $handled; } =head1 Internal Subroutines =head2 load_feed_cache =cut sub load_feed_cache { my ($cui,$cachefile,$nick) = @_; my $l = $cui->userdata->{log}; my $headers; $l->log( level => "debug", message => "$nick: Reading cached feed header data"); if (-e $cachefile) { open (CACHE,"<",$cachefile) or $l->log( level => "warning", message => "Can't open feed cache file $cachefile"); $headers = { If_None_Match => , If_Last_Modified => }; close CACHE; chomp $headers->{If_None_Match} if (defined $headers->{If_None_Match}); chomp $headers->{If_Last_Modified} if (defined $headers->{If_Last_Modified}); } return $headers; } =head2 ping_feed =cut sub ping_feed { my ($cui, $nick, $nomatch, $lastmod) = @_; my $c = $cui->userdata->{c}; my $l = $cui->userdata->{log}; my $ua = $cui->userdata->{ua}; my $cachefile = $cui->userdata->{feeds} . "/$nick.cache"; # get current feed status $l->log( level => "debug", message => "$nick: Sending HEAD request"); my $req = HTTP::Request->new( HEAD => $c->{feeds}{$nick}{feed} ); $req->header(If_None_Match => $nomatch); $req->header(If_Last_Modified => $lastmod); my $res = $ua->simple_request($req); # store new headers for next time if we have them $l->log( level => "debug", message => "$nick: Storing headers in cache file $cachefile"); if ($res->is_success) { open (CACHE,">",$cachefile) or $l->log( level => "warning", message => "Can't open feed cache file $cachefile"); print CACHE $res->header('ETag'), "\n"; print CACHE $res->header('Last-Modified'), "\n"; close CACHE; } return $res } =head2 handle_feed This rountine is the heart of the revamped HTTP processing. It handles most of the real-world possible HTTP response codes. Returns 0 to indicate that a redirect has occurred and processing should restart, otherwise returns the actual HTTP response code. Currently handled codes: 200 OK 301 Permanent Redirect 302 Temporary Redirect (300, 307 treated as 302) 304 Not Modified 404 Not Found 410 Gone 4xx Treated as generic client error 500 Internal Server Error 5xx Treated as generic server error Any other response will be treated as unhandled, and a generic failure. 200 triggers execution of L. 404, 4xx, 500, 5xx, and unhandled response codes trigger execution of the L subroutine. =cut sub handle_feed { my ($res,$cui,$c,$nick) = @_; my $hrc = $res->code; my $l = $cui->userdata->{log}; $l->log( level => "debug", message => "$nick: Handling HTTP response code"); if ($hrc == 200) { # 200 OK get_feed($cui,$nick); $l->log( level => "info", message => "$nick: 200 OK" ); return $hrc; } elsif ($hrc == 301) { # 301 Permanent Redirect # Get new location (store it on success), and reprocess my $loc = $res->header('Location'); $c->{feeds}{$nick}{permfeed} = $c->{feeds}{$nick}{feed} unless (defined $c->{feeds}{$nick}{permfeed}); $c->{feeds}{$nick}{feed} = $loc; $l->log( level => "warning", message => "$nick: 301 REDIRECT - $loc" ); return 0; } elsif ($hrc == 302 or $hrc == 307 or $hrc == 300) { # 302 Temporary Redirect (also 307, 300) # Fetch from new location and reprocess but don't change URL my $loc = $res->header('Location'); $c->{feeds}{$nick}{origfeed} = $c->{feeds}{$nick}{feed} unless (defined $c->{feeds}{$nick}{origfeed}); $c->{feeds}{$nick}{feed} = $loc; $l->log( level => "info", message => "$nick: 302 TEMP REDIRECT - $loc" ); return 0; } elsif ($hrc == 304) { if ($c->{feeds}{$nick}{force}) { get_feed($cui,$nick); $l->log( level => "info", message => "$nick: 304 BUT FORCED - Fetching anyway" ); return $hrc; } else { # 304 Not Modified # Do nothing but mark feed as successfully checked (unless forced) $c->{feeds}{$nick}{last} = time(); $c->write; $l->log( level => "info", message => "$nick: 304 NOT MODIFIED" ); return $hrc; } } elsif ($hrc == 404) { # 404 Not Found # Increment failure count by one and return unless fail threshold is exceeded $l->log( level => "warning", message => "$nick: 404 NOT FOUND" ); failure_handler($cui,$nick); return $hrc; } elsif ($hrc == 410) { # 410 Gone # Mark feed as dormant and inform user $c->{feeds}{$nick}{dormant} = 1; $c->write; $l->log( level => "warning", message => "$nick: 410 GONE - Marked as dormant" ); feed_err_gone($cui,$nick); return $hrc; } elsif ($hrc >= 400 and $hrc < 500) { # Generic client-side error $l->log( level => "warning", message => "$nick: $hrc UNHANDLED CLIENT ERROR" ); failure_handler($cui,$nick); return $hrc; } elsif ($hrc == 500) { # 500 Internal Server Error # Increment failure count by one and return unless fail threshold is exceeded $l->log( level => "warning", message => "$nick: 500 TIMEOUT / SERVER ERROR" ); failure_handler($cui,$nick); return $hrc; } elsif ($hrc > 500) { # Generic server-side error $l->log( level => "warning", message => "$nick: $hrc UNHANDLED SERVER ERROR" ); failure_handler($cui,$nick); return $hrc; } else { # Got a weird one... $l->log( level => "warning", message => "$nick: $hrc UNHANDLED HTTP RESPONSE" ); failure_handler($cui,$nick); return $hrc; } } =head2 get_feed =cut sub get_feed { my ($cui,$nick) = @_; my $c = $cui->userdata->{c}; my $l = $cui->userdata->{log}; my $ua = $cui->userdata->{ua}; my $feedfile = join('/',$cui->userdata->{feeds},$nick); $l->log( level => "debug", message => "$nick: Fetching feed over network"); my $msg = join(' ','Fetching',$c->{feeds}{$nick}{disp},'...'); winstatmsg($cui,$msg); my $rc = $ua->get($c->{feeds}{$nick}{feed}, ':content_file' => $feedfile); if ($rc->is_success) { $l->log( level => "debug", message => "$nick: Feed successfully fetched"); $c->{feeds}{$nick}{last} = time(); $c->{feeds}{$nick}{failure} = 0; } else { # sudden error -- we just pinged this feed! $l->log( level => "warning", message => ("$nick: ERROR IN FETCH - " . $rc->status_line) ); failure_handler($cui,$nick); return; } # unset perm feed url for 301s, as the redirect was successful if (defined $c->{feeds}{$nick}{permfeed}) { $l->log( level => "debug", message => "$nick: Unstoring original URL after successful 301 redirect"); delete $c->{feeds}{$nick}{permfeed}; } # reset feed url for 302s if (defined $c->{feeds}{$nick}{origfeed}) { $l->log( level => "debug", message => "$nick: Restoring original URL after 302 redirect"); $c->{feeds}{$nick}{feed} = $c->{feeds}{$nick}{origfeed}; delete $c->{feeds}{$nick}{origfeed}; } } =head2 failure_handler This routine is called when a feed cannot be fetched for any reason other than a response code of 410 Gone. First the feed's failure count is incremented by one and then its TTL is modified as follows: Fail TTL Mod ---- -------------------------- 1 Set to 0 (Immediate retry) 2 Original TTL * 0.75 3 Original TTL * 1.5 4 Original TTL * 2 5 Feed marked dormant After this, a check is made to see if we were attempting to handle a 301 Permanent Redirect; if so then the feed's original URL is restored so as not to destroy user-entered data on an unsuccessful redirect. Finally, the feed's last-checked time is set so that the TTL modifications will have the intended effect even though a successful check was not made. =cut sub failure_handler { my ($cui,$nick) = @_; my $c = $cui->userdata->{c}; my $l = $cui->userdata->{log}; $l->log( level => "debug", message => "$nick: Handling failure status"); $c->{feeds}{$nick}{failure}++; my $fc = $c->{feeds}{$nick}{failure}; if ($fc == 1) { $c->{feeds}{$nick}{origttl} = $c->{feeds}{$nick}{ttl}; $c->{feeds}{$nick}{ttl} = 0; } elsif ($fc == 2) { $c->{feeds}{$nick}{ttl} = int( $c->{feeds}{$nick}{origttl} * 0.75 ); } elsif ($fc == 3) { $c->{feeds}{$nick}{ttl} = int( $c->{feeds}{$nick}{origttl} * 1.5 ); } elsif ($fc == 4) { $c->{feeds}{$nick}{ttl} = int( $c->{feeds}{$nick}{origttl} * 2 ); } elsif ($fc == 5) { $c->{feeds}{$nick}{ttl} = $c->{feeds}{$nick}{origttl}; delete $c->{feeds}{$nick}{origttl}; $c->{feeds}{$nick}{dormant} = 1; feed_err_dormant($cui,$nick); } # reset feed url for 301s, as the redirect was unsuccessful if (defined $c->{feeds}{$nick}{permfeed}) { $l->log( level => "warning", message => "$nick: Restoring original URL after failed 301 redirect"); $c->{feeds}{$nick}{feed} = $c->{feeds}{$nick}{permfeed}; delete $c->{feeds}{$nick}{permfeed}; } $l->log( level => "warning", message => "$nick: Failure $fc - TTL set to $c->{feeds}{$nick}{ttl}" ); $c->{feeds}{$nick}{last} = time(); $c->write; } =head2 feed_err_dormant Alerts user that a feed has been marked dormant due to repeated retrieval failures. =cut sub feed_err_dormant { my ($cui, $nick) = @_; my $err = <userdata->{wins}; my $c = $cui->userdata->{c}; if($cui->getobj('opts')) { $cui->getobj('opts')->focus; $cui->getobj('opts')->draw; return; } # build hash of enabled options from config object my %selected = (); my $i = 0; foreach my $opt (@{$cui->userdata->{opts}}) { $selected{$i} = 1 if $c->{$opt}; $i++; } my $opts = $cui->add('opts', 'Window', -border => 1, -bfg => 'blue', -title => 'Options', -height => 16, -width => 35, -centered => 1, ); $opts->{list} = $opts->add('obli', 'Listbox', -values => $cui->userdata->{opts}, -labels => {'sls' => 'Show story list status', 'snu' => 'Skip unread on prev/next', 'knf' => 'Keep stories until read', 'gsp' => 'Global story paging', 'dst' => 'Hide titlebar', 'coe' => 'Confirm on exit', }, -multi => 1, -selected => \%selected, -height => 6, -width => 30, -y => 1, -x => 2, -onchange => sub { saveopts($cui,$opts->{list}) }, ); $opts->{mins} = $opts->add('obmi', 'TextEntry', -x => 1, -y => -6, -width => 4, -maxlength => 3, -reverse => 1, -text => $c->{pw}, -onchange => sub { if ($opts->{mins}->get =~ /\D/) { $opts->{mins}->text(''); $opts->{mins}->text($c->{pw}) if $c->{pw}; } elsif ($opts->{mins}->get == 0) { $opts->{mins}->text(''); } else { $c->{pw} = $opts->{mins}->get; &OliveMisc::setpollwait($cui); } }, ); $opts->{l3} = $opts->add(undef, 'Label', -text => "Poll wait (in minutes)", -x => 6, -y => -6, ); $opts->{secs} = $opts->add('obse', 'TextEntry', -x => 1, -y => -5, -width => 4, -maxlength => 3, -reverse => 1, -text => $c->{to}, -onchange => sub { if ($opts->{secs}->get =~ /\D/) { $opts->{secs}->text(''); $opts->{secs}->text($c->{to}) if $c->{to}; } else { $c->{to} = $opts->{secs}->get; } }, ); $opts->{l5} = $opts->add(undef, 'Label', -text => "Fetch timeout (in secs)", -x => 6, -y => -5, ); $opts->{www} = $opts->add('owww', 'TextEntry', -x => 1, -y => -3, -width => 16, -reverse => 1, -text => $c->{www}, -onchange => sub { $c->{www} = $opts->{www}->get; }, ); $opts->{l4} = $opts->add(undef, 'Label', -text => "Link command", -x => 18, -y => -3, ); $opts->{okb} = $opts->add('obok', 'Buttonbox', -y => -1, -x => 24, -buttons => [ { -label => '< Save >', -value => 1, -onpress => sub { $opts->loose_focus; $c->write; $cui->enable_timer('clock'); $cui->delete('opts'); $cui->draw }, } ], ); $cui->disable_timer('clock'); $opts->draw; $opts->focus; } sub saveopts { my ($cui,$o) = @_; my $c = $cui->userdata->{c}; my @optlist = @{$cui->userdata->{opts}}; my @opts = $o->get; my %selopts = (); # build hash of selections, all turned off foreach my $opt (@optlist) { $selopts{$opt} = 0; } # loop through list of all options and list of widget-enabled # options. set the ones which match to 1 (on) foreach my $opt (@optlist) { foreach my $opt2 (@opts) { $selopts{$opt} = 1 if ($opt eq $opt2); } } # now loop through all opts, turning them on or off in the # config onject as appropriate foreach my $opt (@optlist) { $c->{$opt} = $selopts{$opt}; } } =head1 COPYRIGHT & LICENSE Copyright 2005 Shawn Boyette, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut 1; olive-1.3/OliveEdit.pm0000644000175000017500000002674510374275165013431 0ustar mdximdxipackage OliveEdit; require Exporter; use warnings; use strict; our @ISA = qw(Exporter); our @EXPORT = qw( &deletefeed &editfeed ); #-------------------------------------------------------------- sub editfeed { my $cui = shift; my $w = $cui->userdata->{wins}; my $c = $cui->userdata->{c}; if($cui->getobj('editwin')) { $cui->getobj('editwin')->focus; $cui->getobj('editwin')->draw; return; } # build hash of options from config object my ($values,$labels) = feedlist($c); my $edit = $cui->add('editwin', 'Window', -border => 1, -bfg => 'blue', -title => 'Edit Feeds', -height => 18, -width => 30, -centered => 1, ); $edit->{l1} = $edit->add(undef, 'Label', -text => 'Feed Status', -x => 2, -y => 1, -bold => 1, ); $edit->{list} = $edit->add('edli', 'Listbox', -values => $values, -labels => $labels, -height => 10, -width => 25, -y => 3, -x => 2, -vscrollbar => 1, -onchange => sub { editsingle($cui,$edit->{list}) }, ); $edit->{okb} = $edit->add('edok', 'Buttonbox', -y => -2, -x => 20, -buttons => [ { -label => '< OK >', -value => 1, -onpress => sub { $edit->loose_focus; $c->write; $cui->enable_timer('clock'); $cui->delete('editwin'); $cui->draw }, } ], ); $cui->disable_timer('clock'); $edit->draw; $edit->focus; } sub editsingle { my ($cui,$e) = @_; my $c = $cui->userdata->{c}; my $nick = $e->get; return unless (defined $nick); my %f; my %d; my %fl; $f{0} = 0; $f{0} = 1 if $c->{feeds}{$nick}{force}; $d{0} = 0; $d{0} = 1 if $c->{feeds}{$nick}{dormant}; $fl{0} = 0; $fl{0} = 1 if $c->{feeds}{$nick}{flag}; if($cui->getobj('feed')) { $cui->getobj('feed')->focus; $cui->getobj('feed')->draw; return; } my $fw = $cui->add('feed', 'Window', -border => 1, -bfg => 'blue', -title => "Edit $c->{feeds}{$nick}{disp}", -height => 15, -width => 60, -centered => 1, ); $fw->{l1} = $fw->add(undef, 'Label', -text => "Location:", -x => 1, -y => 1, ); $fw->{l2} = $fw->add(undef, 'Label', -text => "Nickname:", -x => 1, -y => 3, ); $fw->{loc} = $fw->add('fwlo', 'TextEntry', -x => 11, -y => 1, -width => 46, -reverse => 1, -text => $c->{feeds}{$nick}{feed}, ); $fw->{nic} = $fw->add('fwnn', 'TextEntry', -x => 11, -y => 3, -width => 17, -maxlength => 16, -reverse => 1, -text => $c->{feeds}{$nick}{disp}, ); $fw->{ffl} = $fw->add('fwfl', 'Listbox', -values => [ 1 ], -labels => { 1 => 'Flag this feed' }, -selected => \%fl, -multi => 1, -height => 1, -width => 27, -y => 5, -x => 11, ); $fw->{frc} = $fw->add('fwfr', 'Listbox', -values => [ 1 ], -labels => { 1 => 'Force-poll this feed' }, -selected => \%f, -multi => 1, -height => 1, -width => 27, -y => 7, -x => 11, ); $fw->{fdr} = $fw->add('fwdr', 'Listbox', -values => [ 1 ], -labels => { 1 => 'Feed is dormant' }, -selected => \%d, -multi => 1, -height => 1, -width => 27, -y => 9, -x => 11, ); $fw->{okb} = $fw->add('fwok', 'Buttonbox', -y => -2, -x => 40, -buttons => [ { -label => '< OK >', -value => 1, -onpress => sub { &OliveFeed::storefeed($cui, $fw->{loc}->get, $fw->{nic}->get, $fw->{frc}->get || 0, $fw->{fdr}->get || 0, $fw->{ffl}->get || 0, $nick); unless ($fw->{fdr}->get) { &OliveFeed::feedpoll($cui); &OliveFeed::refreshlist($cui); } $fw->loose_focus; $cui->delete('feed'); $e->set_selection(-10061); $cui->delete('editwin'); editfeed($cui) } }, { -label => '< Cancel >', -value => 0, -onpress => sub { $fw->loose_focus; $cui->delete('feed'); $e->set_selection(-10061); $cui->draw }, }, ], ); $fw->draw; $fw->{loc}->focus; } sub deletefeed { my $cui = shift; my $w = $cui->userdata->{wins}; my $c = $cui->userdata->{c}; if($cui->getobj('opts')) { $cui->getobj('opts')->focus; $cui->getobj('opts')->draw; return; } # build hash of options from config object my ($values,$labels) = feedlist($c); my $opts = $cui->add('delf', 'Window', -border => 1, -bfg => 'blue', -title => 'Remove Feeds', -height => 18, -width => 34, -centered => 1, ); $opts->{l1} = $opts->add(undef, 'Label', -text => 'Sel', -x => 2, -y => 1, -bold => 1, ); $opts->{l2} = $opts->add(undef, 'Label', -text => 'Feed Status', -x => 6, -y => 1, -bold => 1, ); $opts->{list} = $opts->add('obli', 'Listbox', -values => $values, -labels => $labels, -multi => 1, -height => 11, -width => 29, -y => 2, -x => 2, -vscrollbar => 1, ); $opts->{okb} = $opts->add('obok', 'Buttonbox', -y => -2, -x => 24, -buttons => [ { -label => '< OK >', -value => 1, -onpress => sub { $opts->loose_focus; saveopts($cui,$opts->{list}); $c->write; $cui->enable_timer('clock'); $cui->delete('delf'); &OliveFeed::feedpoll($cui); &OliveFeed::refreshlist($cui); $cui->draw }, } ], ); $cui->disable_timer('clock'); $opts->draw; $opts->focus; } sub saveopts { my ($cui,$o) = @_; my $c = $cui->userdata->{c}; my $d = $cui->userdata->{dbh}; my @opts = $o->get; # destroy feeds foreach my $opt (@opts) { # clear out db entries my $statement = "DELETE FROM stories WHERE nick = '$opt'"; my $sth = $d->prepare($statement); $sth->execute; # delete from HoH delete $c->{feeds}{$opt}; # delete feed files my $feed = $cui->userdata->{feeds} . "/$opt"; my $cache = $cui->userdata->{feeds} . "/$opt.cache"; unlink ($feed, $cache); } } sub feedlist { my ($c) = @_; my @v = (); my %l = (); my $i = 0; foreach my $opt (sort(keys %{$c->{feeds}})) { $v[$i] = $opt; my $label = $c->{feeds}{$opt}{disp} . ' ' x (21 - 3 - length($c->{feeds}{$opt}{disp})) . ($c->{feeds}{$opt}{flag} ? 'f' : '-') . ($c->{feeds}{$opt}{force} ? 'p' : '-') . ($c->{feeds}{$opt}{dormant} ? 'd' : '-'); $l{$v[$i]} = $label; $i++; } return(\@v,\%l); } 1; =head1 COPYRIGHT & LICENSE Copyright 2005,2006 Shawn Boyette, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut olive-1.3/OliveMisc.pm0000644000175000017500000004743310626577122013432 0ustar mdximdxipackage OliveMisc; =head1 NAME OliveMisc - Utility routines for Olive =head1 DESCRIPTION Storage area for miscellaneous utility routines needed by other parts of Olive. =head1 SUBROUTINES Exported: L, L, L, L, L, L, L Unexported: L, L, L, L =cut require Exporter; use warnings; use strict; use Date::Calc qw(Mktime); use Log::Dispatch::File; use OliveEdit; use OliveOpts; our @ISA = qw(Exporter); our @EXPORT = qw( &sysinit &sysinit2 &setpollwait &setkeys &db_fastmode &db_safemode &errorbox &rfc822 &iso8601 ); my %months = ( 'Jan' => 1, 'Feb' => 2, 'Mar' => 3, 'Apr' => 4, 'May' => 5, 'Jun' => 6, 'Jul' => 7, 'Aug' => 8, 'Sep' => 9, 'Oct' => 10, 'Nov' => 11, 'Dec' => 12 ); my %z = ( 'GMT' => 0, 'U' => 0, 'UTC' => 0, 'WET' => 0, 'Z' => 0, 'JST' => 9, 'EST' => -5, 'EDT' => -4, 'CST' => -6, 'CDT' => -5, 'MST' => -7, 'MDT' => -6, 'PST' => -8, 'PDT' => -7, 'AKST'=> -9, 'AKDT'=> -8, ); #------------------------------------------------------------- sub sysinit { my $dotdir = $ENV{HOME} . "/.olive"; my $feeddir = $ENV{HOME} . "/.olive/feeds"; my $dotfile = $dotdir . "/olive.yaml"; my $errfile = $dotdir . "/errors.log"; my @t = localtime(); # set up dotdirm feeddir, dotfile, and errlog mkdir $dotdir, 0700 unless (-e $dotdir); mkdir $feeddir, 0755 unless (-e $feeddir); system("touch $dotfile") unless (-e $dotfile); system("cp $errfile.1 $errfile.2") if (-e "$errfile.1"); system("cp $errfile $errfile.1") if (-e $errfile); open STDERR, '>', $errfile; } #------------------------------------------------------------- sub sysinit2 { my $cui = shift; my $c = $cui->userdata->{c}; my $dbh = $cui->userdata->{dbh}; # set up logging $cui->userdata->{log} = Log::Dispatch::File->new( name => 'olivelog', min_level => $c->{loglevel} || 'warning', filename => ($ENV{HOME} . '/.olive/errors.log'), mode => 'write', callbacks => \&OliveMisc::logstamper, ); # and log startup message my $rel = $cui->userdata->{rel}; my $rev = $cui->userdata->{rev}; $cui->userdata->{log}->log( level => 'warning', message => "Olive $rel (r$rev) starting up." ); # say hello if we're new unless ($cui->userdata->{c}->{fr}) { $cui->dialog( -bfg => 'blue', -message => " This is Olive\n A Totally Sweet Newsfeeder\n\nPress 'h' or '?' anytime for help."); $cui->userdata->{c}->{fr} = $cui->userdata->{rel}; } # set up the db if it isn't (we're new, or it has been deleted) my $statement = 'SELECT count(id) FROM stories'; my ($rows) = $dbh->selectrow_array($statement); if ($rows eq '') { # it isn't. make it. $statement = 'CREATE TABLE stories (id INTEGER PRIMARY KEY, nick TEXT, timestamp INT, md5 TEXT, read INT, new INT, star INT, link TEXT, title TEXT, desc TEXT)'; my $sth = $dbh->prepare($statement); $sth->execute; $statement = 'CREATE TABLE links (sid INTEGER, num INT, link TEXT, desc TEXT)'; $sth = $dbh->prepare($statement); $sth->execute; $statement = 'CREATE INDEX linkidx ON links (sid)'; $sth = $dbh->prepare($statement); $sth->execute; $cui->userdata->{c}->{fr} = $cui->userdata->{rel}; } else { # it is. clean shit up, y0 $statement = "VACUUM"; my $sth = $dbh->prepare($statement); $sth->execute; } # if the user last ran an older version, tell him to delete the db and restart if (($cui->userdata->{c}->{fr} eq 'b10') or ($cui->userdata->{c}->{fr} eq '1')) { errorbox($cui,'There are incompatible database changes in this release of Olive. Please delete ~/.olive/stories.db and restart. Olive will terminate when you dismiss this message.','DB Upgrade'); quitit($cui); } # set feed fetch timeout if there isn't one defined $cui->userdata->{c}->{to} = 30 unless ($cui->userdata->{c}->{to}); # and set story paging/status display if they don't exist $cui->userdata->{c}->{sls} = 1 unless (exists $cui->userdata->{c}->{sls}); $cui->userdata->{c}->{gsp} = 1 unless (exists $cui->userdata->{c}->{gsp}); $cui->userdata->{c}->write; # turn on utf8 if appropriate $cui->userdata->{utf8} = 0; $cui->userdata->{utf8} = 1 if ($ENV{LC_ALL} and ($ENV{LC_ALL} =~ /UTF-8/) or $ENV{LANG} and ($ENV{LANG} =~ /UTF-8/)); } #------------------------------------------------------------- sub db_fastmode { my ($d) = @_; $d->{AutoCommit} = 0; $d->do("PRAGMA default_synchronous = OFF"); } #------------------------------------------------------------- sub db_safemode { my ($d) = @_; $d->commit; $d->{AutoCommit} = 1; $d->do("PRAGMA default_synchronous = ON"); } #------------------------------------------------------------- sub errorbox { my ($cui,$msg,$title) = @_; $cui->disable_timer('clock'); if($cui->getobj('__window_Dialog::Basic')){ $cui->getobj('__window_Dialog::Basic')->focus; $cui->getobj('__window_Dialog::Basic')->draw; return; } $title = "Error" unless (defined $title); $cui->dialog( -title => $title, -message => $msg); $cui->enable_timer('clock'); } #------------------------------------------------------------- sub followlink { my ($cui,$link) = @_; unless ($cui->userdata->{c}->{www} && $cui->userdata->{c}->{www} =~ /LINK/) { errorbox($cui,"You must properly define a link command in the Options panel to enable this function."); return; } unless ($link) { my $d = $cui->userdata->{dbh}; my $w = $cui->userdata->{wins}; my $id = $w->{news}{list}->get; $link = $d->selectrow_array("SELECT link FROM stories WHERE id = $id"); } my $cmd = $cui->userdata->{c}->{www}; $cmd =~ s/LINK/\'$link\'/g; system($cmd); } #------------------------------------------------------------- sub logstamper { my %msghash = @_; my @t = localtime; return sprintf ("[%d-%02d-%02d %02d:%02d:%02d] %s\n", ($t[5] + 1900), ($t[4] + 1), $t[3], $t[2], $t[1], $t[0], $msghash{message}); } #------------------------------------------------------------- sub setpollwait { my $cui = shift; my $c = $cui->userdata->{c}; $c->{pw} = 10 unless ($c->{pw}); my $pw = $c->{pw} * 60; $cui->disable_timer('clock'); $cui->set_timer('clock', sub { &OliveFeed::feedpoll($cui); &OliveStory::refreshlist($cui); &OliveStory::shiftlist($cui); }, $pw); $cui->enable_timer('clock'); } #------------------------------------------------------------- sub quitit { my $cui = shift; my $c = $cui->userdata->{c}; if ($c->{coe}) { my $return = $cui->dialog( -message => "Really quit?", -bfg => "blue", -buttons => ['yes', 'no'], ); exit(0) if $return; } else { exit(0); } } #------------------------------------------------------------- sub setkeys { my ($cui,$root,$docdir) = @_; my $c = $cui->userdata->{c}; my $w = $cui->userdata->{wins}; my $keys = $cui->userdata->{keys}; # global keys, work everywhere all the time # not user-bindable $cui->set_binding(sub { &OliveFeed::getfeed($cui) }, "\cA"); $cui->set_binding(sub { &OliveEdit::deletefeed($cui) },"\cR"); $cui->set_binding(sub { &OliveEdit::editfeed($cui) }, "\cE"); $cui->set_binding(sub { &OliveOpts::setopts($cui) }, "\cO"); $cui->set_binding(sub { &OliveWindow::winredraw($cui,$root) }, "\cL"); $cui->set_binding(sub { aboutpop($cui) }, "\cT"); $cui->set_binding(sub { followlink( $cui, "file:///$docdir/index.html") }, "\cN"); # root window keys, work whenever there isn't a dialog # visible (that's a $cui context) # these are not-user bindable $root->set_binding(sub { quitit($cui) }, 'Q'); $root->set_binding(sub { helppop($cui) }, 'h','?'); # but these are $root->set_binding(sub { &OliveStory::storyprev($cui) }, $keys->{prev} ); $root->set_binding(sub { &OliveStory::storynext($cui) }, $keys->{next} ); $root->set_binding(sub { &OliveStory::markone($cui,1) }, $keys->{mark} ); $root->set_binding(sub { &OliveStory::markone($cui,0) }, $keys->{unmark} ); $root->set_binding(sub { &OliveStory::markall($cui,1) }, $keys->{markall} ); $root->set_binding(sub { &OliveStory::markall($cui,0) }, $keys->{unmarkall} ); $root->set_binding(sub { &OliveStory::starone($cui) }, $keys->{star} ); $root->set_binding(sub { &OliveWindow::winflip($cui) }, $keys->{focus} ); $root->set_binding(sub { followlink($cui) }, $keys->{link} ); $root->set_binding(sub { &OliveWindow::linklist($cui) }, $keys->{linklist} ); $root->set_binding(sub { $cui->userdata->{flag} = $cui->userdata->{flag} ? 0 : 1; &OliveStory::refreshlist($cui) }, $keys->{filterf} ); $root->set_binding(sub { $cui->userdata->{star} = $cui->userdata->{star} ? 0 : 1; &OliveStory::refreshlist($cui) }, $keys->{filters} ); $root->set_binding(sub { &OliveFeed::feedpoll($cui); &OliveStory::refreshlist($cui) }, $keys->{poll} ); $root->set_binding(sub { my $f = &OliveFeed::forcefetch($cui); return errorbox($cui,'There are no feeds marked as forced.','Info') unless ($f); &OliveStory::refreshlist($cui); &OliveStory::shiftlist($cui) }, $keys->{force} ); # and these can be on or off if ($c->{gsp}) { $root->set_binding( sub { $w->{story}{view}->cursor_pagedown; $w->{story}{view}->draw; }, $keys->{gpdn} ); $root->set_binding( sub { $w->{story}{view}->cursor_pageup; $w->{story}{view}->draw; }, $keys->{gpup} ); } } #------------------------------------------------------------- sub rfc822 { # Thu, 14 Apr XX05 10:16:24 GMT my $rfc822 = shift; my $dos = 0; # day offset for substr parsing my $yos = 0; # year offset for substr parsing my @t = (); # time array my @l = localtime; # localtime array # check for day-of-month with no leading zero $t[2] = substr($rfc822,5,2); if ($t[2] =~ /^\d\s$/) { $t[2] = substr($rfc822,5,1); $dos = 1; } # check for 2-digit year $t[0] = substr($rfc822,(12 - $dos),4); if ($t[0] =~ /^\d\d\s/) { $t[0] = substr($rfc822,(12 - $dos),2); $t[0] = "20" . $t[0]; $yos = 2 + $dos; } $t[1] = $months{substr($rfc822,(8 - $dos),3)}; # DOW $t[3] = substr($rfc822,(17 - $yos),2); # HH $t[4] = substr($rfc822,(20 - $yos),2); # MM $t[5] = substr($rfc822,(23 - $yos),2); # SS my $tz = substr($rfc822,(26 - $yos),3); # TZ # check/force chunks of date for validity # use null date if YYYY-MM-DD are missing/invalid return 0 if ($t[0] !~ /\d{4}/ || $t[0] < 1970 || $t[0] > ($l[5] + 1900)); return 0 if ($t[1] =~ /\D/ || $t[1] < 1 || $t[1] > 12); return 0 if ($t[2] =~ /\D/ || $t[2] < 1 || $t[2] > 31); # HH:MM:SS can safely be slammed to 0 for my $i (3..5) { $t[$i] = 0 unless (defined $t[$i] && $t[$i] !~ /\D/); } # munge tz if ($z{$tz}) { $tz = 3600 * $z{$tz}; } elsif ($tz =~ /^[+-]/) { my $secs = $tz =~ /^\+/ ? 3600 : -3600; $tz = $secs * substr($tz,1,2); } else { $tz = 0; } my $time = Mktime(@t); # convert to timestamp $time = $time - $tz; # TZ correction return $time; } #------------------------------------------------------------- sub iso8601 { # 2005-04-14T12:38:51-05:00 my $iso8601 = shift; my @t = (); my @l = localtime; $t[0] = substr($iso8601,0,4); $t[1] = substr($iso8601,5,2); $t[2] = substr($iso8601,8,2); $t[3] = substr($iso8601,11,2); $t[4] = substr($iso8601,14,2); $t[5] = substr($iso8601,17,2); my $sign = substr($iso8601,19,1); my $tz = substr($iso8601,20,2); # check/force chunks of date for validity # use null date if YYYY-MM-DD are missing/invalid return 0 if ($t[0] !~ /\d{4}/ || $t[0] < 1970 || $t[0] > ($l[5] + 1900)); return 0 if ($t[1] =~ /\D/ || $t[1] < 1 || $t[1] > 12); return 0 if ($t[2] =~ /\D/ || $t[2] < 1 || $t[2] > 31); # HH:MM:SS can safely be slammed to 0 for my $i (3..5) { $t[$i] = 0 unless (defined $t[$i] && $t[$i] !~ /\D/); } my $secs = ($sign eq '+') ? 3600 : -3600; $tz = $secs * $tz; my $time = Mktime(@t); # convert to timestamp $time= $time - $tz; # TZ correction return $time; } #------------------------------------------------------------- sub aboutpop { my $cui = shift; return if($cui->getobj('dialog')); my $rel = $cui->userdata->{rel}; my $rev = $cui->userdata->{rev}; my $aboutmsg = <add('about', 'Window', -border => 1, -bfg => 'blue', -title => 'About Olive', -height => 24, -width => 58, -centered => 1, ); $ap->{txt} = $ap->add('atxt', 'TextViewer', -text => $aboutmsg, -border => 1, -height => 21, -width => 56, -vscrollbar => 1, -x => 0, -y => 0, ); $ap->{hkb} = $ap->add('abok', 'Buttonbox', -y => -1, -x => 50, -buttons => [ { -label => '< OK >', -value => 1, -onpress => sub { $ap->loose_focus; $cui->delete('about'); $cui->enable_timer('clock'); $cui->draw }, } ], ); $cui->disable_timer('clock'); $ap->draw; $ap->focus; } #------------------------------------------------------------- sub helppop { my $cui = shift; if($cui->getobj('help')) { $cui->getobj('help')->focus; $cui->getobj('help')->draw; return; } my $keys = $cui->userdata->{keys}; my $helpmsg = <{prev} Previous story $keys->{next} Next story $keys->{mark} Mark as read $keys->{unmark} Mark as unread $keys->{markall} Mark all read $keys->{unmarkall} Mark all unread $keys->{star} Toggle story status (star/unstar) $keys->{filters} Toggle starred story filter (show only starred stories/show all) $keys->{filterf} Toggle flagged feeds filter $keys->{poll} Refresh news list $keys->{force} Refresh forced feeds $keys->{link} Pass link to browser $keys->{focus} Switch pane focus $keys->{linklist} Show story text linklist h,? Show this message C-n Read manual (if extermal browser is defined) C-t Show About box ---- STORY LIST KEYS --------------------------------- Up,Dn Move story selector PgUp Scroll list one screen up PgDn Scroll list one screen down ENTER Read story ---- STORY VIEW KEYS --------------------------------- Up,k Scroll text one line up Dn,j Scroll text one line down PgUp Scroll text one screen up PgDn Scroll text one screen down ---- SEARCH KEYS ------------------------------------- / Start new search (works in all lists and in the story pane) n Repeat last search forward N Repeat last search backward ENTER Select current match; exit search q Exit search without selection ------------------------------------------------------ Still having problems? Email me: HELP 1; my $help = $cui->add('help', 'Window', -border => 1, -bfg => 'blue', -title => 'Help', -height => 20, -width => 61, -centered => 1, ); $help->{txt} = $help->add('htxt', 'TextViewer', -text => $helpmsg, -border => 1, -height => 14, -width => 57, -vscrollbar => 1, -x => 1, -y => 1, ); $help->{hkb} = $help->add('hbok', 'Buttonbox', -y => -2, -x => 52, -buttons => [ { -label => '< OK >', -value => 1, -onpress => sub { $help->loose_focus; $cui->delete('help'); $cui->enable_timer('clock'); $cui->draw }, } ], ); $cui->disable_timer('clock'); $help->draw; $help->focus; } =head1 COPYRIGHT & LICENSE Copyright 2005 Shawn Boyette, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut 1; olive-1.3/INSTALL0000644000175000017500000000136010364046347012217 0ustar mdximdxi INSTALL FOR OLIVE ----------------- Follow the instructions in ./docs/start.html BUT... If you want the keystroke-based poll inactivity feature (prevents feed polling from occurring for 30s after your last keypress) to work, you'll need to patch your C::UI install. >>> NOTE: If you have patched UI.pm, there is <<< >>> no need to do so again. <<< Locate your copy of Curses::UI. Good places to look are: /usr/share/perl/Curses/UI.pm /usr/local/share/perl/5.x.x/Curses/UI.pm Once found, apply the included diff. Example: patch /usr/share/perl/Curses/UI.pm UI.pm.diff This patch is now in the CVS version of Curses::UI, but is not yet in a public release. olive-1.3/OliveXML.pm0000644000175000017500000002261110374275321013162 0ustar mdximdxipackage OliveXML; require Exporter; use warnings; use strict; use Digest::MD5 qw(md5_hex); use Encode qw(encode_utf8); use OliveMisc; use XML::Simple; our @ISA = qw(Exporter); our @EXPORT = qw( &parsexml ); #------------------------------------------------------------- sub parsexml { my ($cui,$nick) = @_; my $c = $cui->userdata->{c}; my $d = $cui->userdata->{feeds}; my $l = $cui->userdata->{log}; my $f = ''; # actually parse the feed eval { $f = XMLin("$d/$nick"); }; if ($@) { $l->log( level => "warning", message => "$nick: XML parse error $@"); return; } my %data = (); my $md5s = 0; if ($f->{xmlns} && $f->{xmlns} =~ /atom/) { # We don't do ATOM right now. errorbox($cui,"Olive does not handle ATOM feeds. This feed will be marked dormant.",$nick); $c->{feeds}{$nick}{dormant} = 1; } elsif ($f->{version}) { # feed is RSS(?) ################################################## $data{title} = $f->{channel}{title}; $data{ttl} = 3600; $data{ttl} = $f->{channel}{ttl} * 60 if ($f->{channel}{ttl}); # iterate over the feed items, get back a list of md5sums $md5s = parseitems($f->{channel}{item},$cui,$nick); } elsif ($f->{channel}{'dc:date'} or $f->{channel}{'rdf:about'}) { # feed is RDF 0.9/1.0 $data{title} = $f->{channel}{title}; # hash out TTL $data{ttl} = 86400; if ($f->{channel}{'syn:updatePeriod'}) { my $freq = $f->{channel}{'syn:updatePeriod'}; $data{ttl} = 3600 if ($freq eq 'hourly'); $data{ttl} = 86400 if ($freq eq 'daily'); $data{ttl} = 86400 * 7 if ($freq eq 'weekly'); $data{ttl} = 86400 * 7 * 30 if ($freq eq 'monthly'); } if ($f->{channel}{'syn:updateFrequency'}) { $data{ttl} = int( $data{ttl} / $f->{channel}{'syn:updateFrequency'} ); } # iterate over the feed items, get back a list of md5sums $md5s = parseitems($f->{item},$cui,$nick); } else { $c->{feeds}{$nick}{failures}++; if ($c->{feeds}{$nick}{failures}++ == 5) { my $disp= $c->{feeds}{$nick}{disp}; my $msg = "There have been problems parsing ${disp}. " . "Either Olive doesn't know how to handle its format, or XML " . "parsing errors have occurred repeatedly.\n\n" . "This feed will now be marked as dormant."; errorbox($cui,$msg); $c->{feeds}{$nick}{dormant} = 1; } } # Undo any potential failure count $c->{feeds}{$nick}{failures} = 0; # catch illegal TTL values $data{ttl} = 3600 if (!defined $data{ttl} or $data{ttl} < 1 or $data{ttl} =~ /\D/); return \%data,$md5s; } #------------------------------------------------------------- sub parseitems { my ($items,$cui,$nick) = @_; return unless $items; # if the feed is empty, do nothing. my $c = $cui->userdata->{c}; my $d = $cui->userdata->{dbh}; my $l = $cui->userdata->{log}; my $w = $cui->userdata->{wins}; my %md5s = (); my $i = 1; my $j = 0; # fix single-entry feeds (should be ARRAY not HASH) if (ref($items) eq 'HASH') { my $tmpitems = $items; undef $items; $OliveXML::items->[0] = (); my $k = my $v = 0; while (($k,$v) = each %{$tmpitems}) { $items->[0]{$k} = $v; } } my $count = @{$items}; foreach my $item (@{$items}) { $cui->userdata->{sid} = 0; my $links = []; my @q = qw(NULL NULL NULL NULL NULL NULL NULL NULL NULL); $q[0] = $d->quote($nick); # first, build string for md5sum # FIXME there is definitely a better way/time to do this... my $md5seed = join('',$nick, ($item->{title} || 'NULL'), ($item->{description} || 'NULL')); $q[2] = md5_hex(encode_utf8($md5seed)); # which must be wrapped in UTF-8 for chars above 255 $md5s{$q[2]} = 1; # store a copy in %md5s for later $q[2] = $d->quote($q[2]); # check for matching MD5s (existing entries) and insert if none found my ($xid) = $d->selectrow_array("SELECT id FROM stories WHERE nick = $q[0] and md5 = $q[2]"); unless ($xid) { my $statement = "INSERT INTO stories VALUES (NULL, NULL, NULL, " . "$q[2], NULL, NULL, 0, NULL, NULL, NULL)"; my $sth = $d->prepare($statement); $sth->execute; $cui->userdata->{sid} = $d->last_insert_id(undef,undef,'stories','id'); $j++; } if ($item->{pubDate}) { # turn the RFC-822 date into seconds-since-epoch $q[1] = rfc822($item->{pubDate}); } elsif ($item->{'dc:date'}) { # turn the ISO-8601 date into seconds-since-epoch $q[1] = iso8601($item->{'dc:date'}); } else { # no time given. set to epoch. $q[1] = 0; } $q[3] = 0; # 'read' flag $q[4] = 1; # 'new' flag if ($item->{link}) { $item->{link} =~ s/^\s+//; $item->{link} =~ s/\s+$//; $q[5] = $d->quote($item->{link}) if $item->{link}; } if ($item->{title}) { $item->{title} =~ s/^\s+//; $item->{title} =~ s/\s+$//; ($q[6]) = HTMangLe($item->{title}) if $item->{title}; $q[6] = $d->quote($q[6]) } # mogrify main text if((ref($item->{description})) eq "HASH") { $item->{description} = ''; } if ($item->{description}) { ($q[7],$links) = HTMangLe($item->{description}); $q[7] = $d->quote($q[7]); } # poke the rest of the data into the db if this story's not a dupe if ($cui->userdata->{sid}) { my $statement = "UPDATE stories SET nick = $q[0], timestamp = $q[1], " . "read = $q[3], new = $q[4], link = $q[5], title = $q[6], desc = $q[7] " . 'WHERE id = ' . $cui->userdata->{sid}; my $sth = $d->prepare($statement); $sth->execute; my $i = 0; foreach my $link (@{$links}) { # ...and the internal links, if any $statement = 'INSERT INTO links VALUES (' . $cui->userdata->{sid} . ", $i, " . $d->quote($link->{link}) . ', ' . $d->quote($link->{desc}) . ')'; $sth = $d->prepare($statement); $sth->execute; $i++; } } percent($i,$count,$c->{feeds}{$nick}{disp},$w); $i++; } $l->log( level => "info", message => "$nick: $j new (of $count) stories inserted"); return \%md5s; } #------------------------------------------------------------- sub percent { my ($i,$count,$nick,$w) = @_; my $percent = int(($i / $count) * 100); my $msg = "Parsing $nick [ $i of $count ($percent\%)]"; $w->{news}{ftr1}->text($msg . ' ' x ($w->{dim}[0] - length($msg))); $w->{news}{ftr1}->draw; } #------------------------------------------------------------- sub HTMangLe { my $text = shift; Encode::_utf8_on($text); # strip story URLs and store them my @links = (); my $i = 0; while ($text =~ m|.*?|) { $text =~ s|(.*?)|[$2][$i]|m; $links[$i] = { link => $1, desc => $2, }; $links[$i]->{link} =~ s/^\s+//; $links[$i]->{link} =~ s/\s+$//; $links[$i]->{link} =~ s/\s{2,}/ /; $links[$i]->{desc} =~ s/^\s+//; $links[$i]->{desc} =~ s/\s+$//; $links[$i]->{desc} =~ s/\s{2,}/ /; $i++; } # reformatting $text =~ s##\*#mg; # strong to *strong* $text =~ s##_#mg; # em to _em_ $text =~ s##|#mg; # cite to |cite| $text =~ s//[[img: $1 ]]/mg; $text =~ s|||mg; $text =~ s/\s{2,}/ /mg; # newline mangling $text =~ s|

(.+?)

|\n\n>>> $1\n\n|mg; $text =~ s|(.+?)|\n\n-- $1\n\n|mg; $text =~ s||\n|mg; $text =~ s||\n|mg; $text =~ s||\n|mg; $text =~ s||\n|mg; $text =~ s/
  • \s*/\n * /mg; $text =~ s/\n{2,} \* /\n * /mg; $text =~ s/\n{3,}/\n\n/mg; $text =~ s/^\n//; # strip whatever tags remain $text =~ s|||mg; # entity handling # thanks to TorgoX's unicode sliderule $text =~ s|>|>|mg; $text =~ s|<|<|mg; $text =~ s|\x{2018}|'|mg; $text =~ s|\x{2019}|'|mg; $text =~ s|\x{201c}|"|mg; $text =~ s|\x{201d}|"|mg; $text =~ s|\x{2026}|...|mg; $text =~ s|�?36;|\$|mg; $text =~ s|�?38;|&|mg; $text =~ s|�?39;|'|mg; $text =~ s|–|--|mg; $text =~ s|—|--|mg; $text =~ s|‘|'|mg; $text =~ s|’|'|mg; $text =~ s|“|"|mg; $text =~ s|”|"|mg; $text =~ s|&|&|mg; $text =~ s|©|(c)|mg; $text =~ s|£|L|mg; $text =~ s|"|"|mg; $text =~ s|®|(r)|mg; $text =~ s|&squot;|'|mg; $text =~ s|™|[tm]|mg; return $text,\@links; } 1; =head1 COPYRIGHT & LICENSE Copyright 2005,2006 Shawn Boyette, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut olive-1.3/OliveFeed.pm0000644000175000017500000003051110615365155013366 0ustar mdximdxipackage OliveFeed; =head1 NAME OliveFeed - Feed handling driver code =head1 DESCRIPTION This module contains the code which drives the processing of RSS feeds for Olive. It does not handle actually fetching the feeds (see L), or parsing the feeds once they are downloaded (see L). =head1 SUBROUTINES Exported: L, L Unexported: L, L, L, L =cut require Exporter; use warnings; use strict; use Digest::MD5 qw( md5_hex ); use OliveHTTP; use OliveMisc; use OliveStory; use OliveXML; our @ISA = qw(Exporter); our @EXPORT = qw( &feedpoll &storefeed ); #------------------------------------------------------------- sub feedpoll { my $cui = shift; my $c = $cui->userdata->{c}; my $d = $cui->userdata->{dbh}; my $l = $cui->userdata->{log}; my $w = $cui->userdata->{wins}; my $t = time(); my $i = 0; $l->log( level => "info", message => ">> Polling feeds" ); # setup for statusline output my @lt = localtime($t); my $when = sprintf("%02d:%02d",$lt[2],$lt[1]); $cui->userdata->{pwhen} = $when unless (defined $cui->userdata->{pwhen}); # tune sqlite for speed db_fastmode($d); # walk through feeds, checking delta time against ttl foreach my $nick (keys %{$c->{feeds}}) { if ($c->{feeds}{$nick}{dormant}) { $l->log( level => "info", message => "$nick: Skipping dormant feed" ); next; } my $ttl = $c->{feeds}{$nick}{ttl}; my $last = $c->{feeds}{$nick}{last}; my $delta = $t - $last; if ($delta > $ttl) { $l->log( level => "info", message => "$nick: TTL expired; fetching" ); my $frc = fetchfeed($cui,$nick); if ($frc == 200) { $cui->userdata->{polled}{$nick} = 1; $l->log( level => "info", message => "$nick: Fetch successful; parsing..." ); sourcefeed($cui,$nick); $i++; } else { $l->log( level => "info", message => "$nick: No fetch" ); } } else { $l->log( level => "info", message => "$nick: TTL not expired ($delta/$ttl s)" ); } } # mark new but read stories as old my ($read) = $d->selectrow_array("SELECT count(id) FROM stories WHERE new = 1 AND read = 1"); if ($read) { $l->log( level => "info", message => ">> Flagging $read stories as old" ); $d->do("UPDATE stories SET new = 0 WHERE new = 1 AND read = 1"); } # flush changes to db and restore safe-working settings db_safemode($d); # build polling data string for status line and update my $j = keys %{$cui->userdata->{polled}}; $cui->userdata->{poll} = "[P: $i \@ $when"; $cui->userdata->{poll} .= " | $j since ".$cui->userdata->{pwhen} if ($j); $cui->userdata->{poll} .= ']'; $w->{news}{ftr1}->text(' ' x $w->{dim}[0]); $w->{news}{ftr1}->draw; $l->log( level => "info", message => ">> Poll complete" ); } #------------------------------------------------------------- sub forcefetch { my $cui = shift; my $c = $cui->userdata->{c}; my $i = 0; foreach my $nick (keys %{$c->{feeds}}) { next if $c->{feeds}{$nick}{dormant}; # skip dormant feeds if ($c->{feeds}{$nick}{force}) { fetchfeed($cui,$nick,1); sourcefeed($cui,$nick); $i++; } } return $i; } #------------------------------------------------------------- sub getfeed { my $cui = shift; my $feed = ''; my $feedname = ''; if($cui->getobj('feed')) { $cui->getobj('feed')->focus; $cui->getobj('feed')->draw; return; } my $fw = $cui->add('feed', 'Window', -border => 1, -bfg => 'blue', -title => 'Add New Feed', -height => 13, -width => 60, -centered => 1, ); $fw->{l1} = $fw->add(undef, 'Label', -text => "Location:", -x => 1, -y => 1, ); $fw->{l2} = $fw->add(undef, 'Label', -text => "Nickname:", -x => 1, -y => 3, ); $fw->{loc} = $fw->add('fwlo', 'TextEntry', -x => 11, -y => 1, -width => 46, -reverse => 1, -onchange => sub { $feed = $fw->{loc}->get }, ); $fw->{nic} = $fw->add('fwnn', 'TextEntry', -x => 11, -y => 3, -width => 17, -maxlength => 16, -reverse => 1, -onchange => sub { $feedname = $fw->{nic}->get }, ); $fw->{ffl} = $fw->add('fwfl', 'Listbox', -values => [ 1 ], -labels => { 1 => 'Flag this feed' }, -multi => 1, -height => 1, -width => 27, -y => 5, -x => 11, ); $fw->{frc} = $fw->add('fwfr', 'Listbox', -values => [ 1 ], -labels => { 1 => 'Force-poll this feed' }, -multi => 1, -height => 2, -width => 27, -y => 7, -x => 11, ); $fw->{okb} = $fw->add('fwok', 'Buttonbox', -y => -2, -x => 40, -buttons => [ { -label => '< OK >', -value => 1, -onpress => sub { my $rc = storefeed($cui, $feed, $feedname, $fw->{frc}->get || 0, 0, $fw->{ffl}->get || 0, ); return if $rc; $fw->loose_focus; $cui->enable_timer('clock'); $cui->delete('feed'); feedpoll($cui); refreshlist($cui) }, }, { -label => '< Cancel >', -value => 0, -onpress => sub { $fw->loose_focus; $cui->enable_timer('clock'); $cui->delete('feed'); $cui->draw }, }, ], ); $cui->disable_timer('clock'); $fw->draw; $fw->{loc}->focus; } #------------------------------------------------------------- =head2 prunefeed Remove stale stories from the story database Arguments: Hashref of MD5 digests DBD::SQLite database handle Nick of the current feed Returns: Nothing. Called from: L Calls: Nothing First, the id and md5 fields are selected from the story database for every unstarred (and optionally, previously-read) story in the current feed. This data is iterated over. If a story is starred, it is skipped and stays in the db. If the MD5 digest of the story being examined is found in the hash passed as an argument, then that story is still in the feed, and a flag is set. If the flag is not set, the story is marked for deletion by having its id added to a hash. After all stories have been checked, the deletions hash is iterated over to remove stories from the database which no longer appear in the feed. =cut sub prunefeed { my ($md5s,$cui,$nick) = @_; my $d = $cui->userdata->{dbh}; my $l = $cui->userdata->{log}; my $i = 0; my %delete = (); my $statement = "SELECT id,md5 FROM stories WHERE nick = '$nick' AND star = 0"; $statement .= ' AND read = 1' if $cui->userdata->{c}->{knf}; my $sth = $d->prepare($statement); $sth->execute; while (my $q = $sth->fetchrow_arrayref) { my $keep = 0; foreach my $md5 (keys %{$md5s}) { $keep = 1 if ($md5 eq $q->[1]); } $delete{$q->[0]} = 1 unless $keep; } foreach my $id (keys %delete) { $d->do("DELETE FROM stories WHERE id = $id"); $d->do("DELETE FROM links WHERE sid = $id"); $i++; } $l->log( level => "info", message => "$nick: $i stories fell off feed" ) if $i; } #------------------------------------------------------------- sub sourcefeed { my ($cui,$nick) = @_; my $c = $cui->userdata->{c}; my $w = $cui->userdata->{wins}; # set status area my $disp= $c->{feeds}{$nick}{disp}; my $msg = "Parsing $disp..."; $w->{news}{ftr1}->text($msg . ' ' x ($w->{dim}[0] - length($msg))); $w->{news}{ftr1}->draw; # process feed my ($data,$md5s) = parsexml($cui,$nick); # yank expired stories from the db $msg = "Pruning expired stories from ".$c->{feeds}{$nick}{disp}."..."; $w->{news}{ftr1}->text($msg . ' ' x ($w->{dim}[0] - length($msg))); $w->{news}{ftr1}->draw; prunefeed($md5s,$cui,$nick) if (ref $md5s eq 'HASH'); # set (possibly new) ttl and title $c->{feeds}{$nick}{ttl} = $data->{ttl}; $c->{feeds}{$nick}{title} = $data->{title}; $c->write; # clear status area $w->{news}{ftr1}->text(' ' x $w->{dim}[0]); $w->{news}{ftr1}->draw; } #------------------------------------------------------------- sub storefeed { my ($cui,$feed,$disp,$force,$dormant,$flag,$nick) = @_; my $c = $cui->userdata->{c}; my $ua = $cui->userdata->{ua}; # check for null data if ($feed eq '' or $disp eq '') { errorbox($cui,"Location and Nickname must both have values for feeds to be saved."); return 1; } # trim whitespace $feed =~ s/^\s+//; $feed =~ s/\s+$//; $disp =~ s/^\s+//; $disp =~ s/\s+$//; # save original nick for display and sanitize unless ($nick) { $nick = $disp; $nick =~ s/\s/_/g; $nick =~ s/\W//g; $nick = md5_hex($disp) if ($nick eq ''); $nick = lc($nick); # check for dupe nicks if (defined $c->{feeds}{$nick}) { errorbox($cui,"There is already a feed with that nick."); return 1; } } # check for dupe feeds foreach my $f (keys %{$c->{feeds}}) { if ( ($f ne $nick) && ($feed eq $c->{feeds}{$f}{feed}) ) { errorbox($cui,"There is already a nick with that URI: $f"); return 1; } } # poke it over the network to see if it exists (unless dormant) unless ($dormant) { my $rc = $ua->head($feed); unless ($rc->is_success) { my $error = $rc->status_line; errorbox($cui,"There was a network problem:\n$error"); return 1; } } # everything looks okay. store it. $force = 0 unless (defined $force); $c->{feeds}{$nick}{feed} = $feed; $c->{feeds}{$nick}{disp} = $disp; $c->{feeds}{$nick}{flag} = $flag; $c->{feeds}{$nick}{force} = $force; $c->{feeds}{$nick}{dormant} = $dormant; $c->{feeds}{$nick}{last} = $c->{feeds}{$nick}{last} || 0; $c->{feeds}{$nick}{ttl} = $c->{feeds}{$nick}{ttl} || 0; $c->write; return 0; } =head1 COPYRIGHT & LICENSE Copyright 2005 Shawn Boyette, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut 1; olive-1.3/Makefile0000644000175000017500000000247210364046347012633 0ustar mdximdxiDESTDIR = PREFIX = /usr/local BINDIR = $(PREFIX)/bin LIBDIR = $(PREFIX)/share/olive DOCDIR = $(PREFIX)/share/doc/olive PERL = $(shell which perl) # DO NOT EDIT BELOW THIS LINE DEPS = Config::YAML Curses::UI Date::Calc DBI DBD::SQLite \ LWP::UserAgent XML::Parser XML::Simple \ Log::Dispatch::File MODULES = OliveEdit.pm OliveMisc.pm OliveStory.pm OliveXML.pm \ OliveFeed.pm OliveOpts.pm OliveWindow.pm OliveHTTP.pm \ OliveOPML.pm .PHONY : olive install olive : @echo -n Checking for dependancies... @perl ./depchecks $(DEPS) @echo looks good. install : olive @echo Prepping for install. @cp olive olive.tmp @perl -pi -e 's|^use lib "."|use lib "$(LIBDIR)"|' olive.tmp @perl -pi -e 's|"./docs"|"$(DOCDIR)"|' olive.tmp @perl -pi -e 's|^#!.+|#!$(PERL)|' olive.tmp @echo Creating install directories. @rm -rf $(DESTDIR)$(LIBDIR) @install -d $(DESTDIR)$(PREFIX) $(DESTDIR)$(BINDIR) $(DESTDIR)$(LIBDIR) $(DESTDIR)$(DOCDIR) @echo Installing Olive. @install -m 0755 olive.tmp $(DESTDIR)$(BINDIR)/olive @install -m 0644 $(MODULES) $(DESTDIR)$(LIBDIR) @install -m 0644 ./docs/*html ./docs/*css $(DESTDIR)$(DOCDIR) @echo Cleaning up. @rm olive.tmp @echo Done. Now run $(BINDIR)/olive to get started! uninstall: @echo Uninstalling Olive. @rm $(BINDIR)/olive @rm -rf $(LIBDIR) $(DOCDIR) @echo Done.olive-1.3/olive0000755000175000017500000000762210626577122012242 0ustar mdximdxi#!/usr/bin/perl =head1 NAME Olive -- The Totally Sweet Newsfeeder =head1 DESCRIPTION Olive is a totally sweet console mode RSS aggregator / newsreader written in Perl with Curses::UI as its toolkit. =head1 SYNOPSIS olive # run olive olive # import a feedlist from an OPML file =cut use warnings; use strict; use lib "."; use Config::YAML; use Curses::UI; use DBI; use LWP::UserAgent; use OliveFeed; use OliveMisc; use OliveStory; use OliveWindow; # force XML::Simple to use XML::Parser instead of XML::SAX $XML::Simple::PREFERRED_PARSER = "XML::Parser"; # set uber-config vars my $rel = 'r1'; my $rev = '$Rev: 439 $'; $rev =~ s/\D//g; $rev = uc(sprintf("%4.4x",$rev)); my $tz = `date +%z`; $tz =~ s/00$//; # system init; sysinit(); # OPML import (and help/version messages) if (@ARGV) { showhelp() if ($ARGV[0] eq '-h' or $ARGV[0] eq '--help'); showver($rel, $rev) if ($ARGV[0] eq '-v' or $ARGV[0] eq '--version'); my $opml = shift; require OliveOPML; import OliveOPML; opmlimport($opml); exit; } # create Curses::UI object my $cui = new Curses::UI( -color_support => 1, -clear_on_exit => 1, -keydelay => 30, -mouse_support => 0, ); #create objects, windows and userdata storage my $root = $cui->add('root', 'Window'); my $ud = { dbh => DBI->connect("dbi:SQLite:dbname=$ENV{HOME}/.olive/stories.db","",""), feeds => "$ENV{HOME}/.olive/feeds", focus => 'news', rel => $rel, rev => $rev, opts => [ qw(sls snu knf gsp dst coe) ], tz => $tz, }; $cui->userdata($ud); $cui->userdata->{c} = Config::YAML->new( config => "$ENV{HOME}/.olive/olive.yaml" ); $cui->userdata->{sid} = 0; $cui->userdata->{ua} = LWP::UserAgent->new( agent => 'Olive/' . $cui->userdata->{rel}, timeout => ($cui->userdata->{c}->{to} || 30) ); $cui->userdata->{wins} = wininit($cui,$root); $cui->userdata->{keys} = { prev => '[', next => ']', mark => 'm', unmark => 'u', markall => 'M', unmarkall => 'U', star => 's', focus => 'w', link => 'l', linklist => 'L', poll => 'p', force => 'P', filterf => 'F', filters => 'S', gpup => '-', gpdn => ' ', }; @{ $cui->userdata->{keys} }{ keys %{$cui->userdata->{c}->{keys}} } = values %{ $cui->userdata->{c}->{keys} }; # set up database, keybindings, and timer loop sysinit2($cui); setkeys($cui,$root,"./docs"); setpollwait($cui); # run a poll and populate the story list feedpoll($cui); refreshlist($cui); # go! $cui->mainloop(); #------------------------------------------------------------- sub showhelp { print < | ] If a filename is specified, an OPML feed import will be performed and the program will exit. Otherwise the program will start normally. Options are: -h --help Show this message and exit -v --version Display version string and exit HELP 1; exit; } sub showver { print "This is Olive $_[0] ($_[1])\n"; exit; } =head1 FILES ~/.olive/olive.yaml # config file ~/.olive/stories.db # SQLite feed data db ~/.olive/errors.log # logger messages go here =head1 COPYRIGHT & LICENSE Copyright 2005,2006 Shawn Boyette, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut