File Coverage

blib/lib/App/Nag.pm
Criterion Covered Total %
statement 84 115 73.0
branch 15 26 57.6
condition 9 10 90.0
subroutine 12 14 85.7
pod 3 3 100.0
total 123 168 73.2


line stmt bran cond sub pod time code
1             package App::Nag;
2             BEGIN {
3 1     1   15126 $App::Nag::VERSION = '0.002';
4             }
5              
6             # ABSTRACT: send yourself a reminder
7              
8              
9 1     1   9 use Modern::Perl;
  1         1  
  1         5  
10 1     1   995 use Getopt::Long::Descriptive qw(describe_options prog_name);
  1         130369  
  1         13  
11              
12             # some icon specs
13 1     1   418 use constant PHRASE => [qw(psst hey HEY !!!)];
  1         3  
  1         95  
14 1     1   5 use constant STROKE => [qw(0000ff 0000ff ff0000 ff0000)];
  1         3  
  1         48  
15 1     1   5 use constant FILL => [qw(ffffff ffffff ffffff ffff00)];
  1         2  
  1         38  
16 1     1   5 use constant OPACITY => [ 0, 1, 1, 1 ];
  1         1  
  1         43  
17 1     1   5 use constant FONT_SIZE => [ 20, 25, 28, 32 ];
  1         3  
  1         53  
18 1     1   5 use constant XY => [ [ 8, 40 ], [ 9, 40 ], [ 7, 41 ], [ 3, 43 ] ];
  1         2  
  1         2037  
19              
20              
21             sub validate_args {
22 12     12 1 52291 my $name = prog_name;
23 12         233 my ( $opt, $usage ) = describe_options(
24             "$name %o
25             [],
26             ['Send yourself a reminder.'],
27             [],
28             [
29             'urgency' => hidden => {
30             one_of => [
31             [ 'nudge|n', 'low key reminder' ],
32             [
33             'poke|p',
34             'reminder with no particular urgency (default)'
35             ],
36             [ 'shake|s', 'urgent reminder' ],
37             [ 'slap', 'do this!!!' ],
38             ]
39             }
40             ],
41             [],
42             [ 'help', "print usage message and exit" ],
43             );
44              
45 12 50       27548 print( $usage->text ), exit if $opt->help;
46 12         167 given ( scalar @ARGV ) {
47 12         46 when (0) {
48 0         0 $usage->die(
49             {
50             pre_text => "ERROR: No time or message.\n\n"
51             }
52             )
53             }
54 12         32 when (1) {
55 0         0 $usage->die(
56             {
57             pre_text => "ERROR: No message.\n\n"
58             }
59             )
60             }
61             }
62 12         68 return ( $opt, $usage, $name );
63             }
64              
65              
66             sub validate_time {
67 12     12 1 291 my ( undef, $opt, $usage, $time, @args ) = @_;
68 12         143 require DateTime;
69 12         80 require DateTime::TimeZone;
70              
71             # parse time
72 12 100       88 $usage->die(
73             {
74             pre_text => "ERROR: could not understand time expression: $time\n\n"
75             }
76             ) unless my %props = _parse_time($time);
77 11         84 my $tz = DateTime::TimeZone->new( name => 'local' );
78 11         82917 my $now = DateTime->now( time_zone => $tz );
79 11         3646 my $then = $now->clone;
80              
81 11 100       223 if ( $props{unit} ) {
82 3         10 my $unit = $props{unit};
83 3         7 given ($unit) {
84 3         11 when ('h') { $unit = 'hours' }
  2         7  
85 1         2 when ('m') { $unit = 'minutes' }
  0         0  
86 1         3 when ('s') { $unit = 'seconds' }
  1         5  
87             }
88 3         19 $then->add( $unit => $props{time} );
89             }
90             else {
91 8         38 my ( $hour, $minute ) = @props{qw(hour minute)};
92 8 100 66     97 $usage->die( { pre_text => "ERROR: impossible time\n\n" } )
93             unless $hour < 25 && $minute < 60;
94 7         22 my $suffix = $props{suffix};
95 7 100 100     49 $usage->die( { pre_text => "ERROR: impossible time\n\n" } )
96             if $hour > 12 && $suffix eq 'a';
97 6         33 $then->set( hour => $hour, minute => $minute, second => 0 );
98 5 100       1894 if ( $hour < 13 ) {
99 4         29 $then->add( hours => 12 ) while $then < $now;
100 4         2839 given ($suffix) {
101 4 50       13 when ('a') { $then->add( hours => 12 ) if $then->hour >= 12 }
  3         14  
102 1 0       4 when ('p') { $then->add( hours => 12 ) if $then->hour < 12 }
  0         0  
103             }
104             }
105             else {
106 1 50       8 $then->add( days => 1 ) if $then < $now;
107             }
108             }
109 8         4285 my $seconds = $then->epoch - $now->epoch;
110 8 50       129 $seconds = 0 if $seconds < 0; # same moment
111              
112             # set verbosity level
113 8         11 my $verbosity;
114 8         45 given ( $opt->urgency ) {
115 8         41 when ('nudge') { $verbosity = 0 }
  0         0  
116 8         16 when ('poke') { $verbosity = 1 }
  0         0  
117 8         16 when ('shake') { $verbosity = 2 }
  0         0  
118 8         14 when ('slap') { $verbosity = 3 }
  2         30  
119 6         7 default { $verbosity = 1 }
  6         18  
120             };
121              
122             # generate message text and synopsis
123 8         28 my $text = join ' ', @args;
124 8         68 $text =~ s/^\s++|\s++$//g;
125 8         62 $text =~ s/\s++/ /g;
126 8         54 ( my $synopsis = $text ) =~ s/^(\S++(?: \S++){0,3}).*/$1/;
127 8 50       30 $synopsis .= ' ...' if length($text) - length($synopsis) > 4;
128 8         121 return ( $verbosity, $text, $synopsis, $seconds );
129             }
130              
131             # extract useful bits out of a time expression
132             # tried to do this with a more readable recursive regex and callbacks but got
133             # OOM errors at unpredictable intervals so I gave up
134             sub _parse_time {
135 12     12   21 my %props;
136 12         25 given ( $_[0] ) {
137 12         65 when (/^(\d++)([hms])$/i) { @props{qw(time unit)} = ( $1, lc $2 ) }
  3         35  
138 9         75 when (/^(\d{1,2})(?::(\d{2}))?(?:([ap])(\.?)m\4)?$/i) {
139 8   100     134 @props{qw(hour minute suffix)} = ( $1, $2 || 0, lc( $3 || '' ) )
      100        
140             }
141             }
142 12         109 return %props;
143             }
144              
145              
146             sub run {
147 0     0 1   my ( undef, $name, $seconds, $text, $synopsis, $verbosity ) = @_;
148 0 0         unless (fork) {
149 0           require Gtk2::Notify;
150 0           Gtk2::Notify->init($name);
151              
152 0           sleep $seconds;
153              
154 0           my $icon = _icon( $name, $verbosity );
155 0 0         if ( $verbosity == 3 ) {
156 0           require App::Nag::Slap;
157 0           App::Nag::Slap->run( $synopsis, $text, $icon );
158             }
159             else {
160 0           Gtk2::Notify->new( $synopsis, $text, $icon )->show;
161             }
162 0           unlink $icon;
163             }
164             }
165              
166             # make a somewhat eye-catching icon
167             sub _icon {
168 0     0     my ( $name, $verbosity ) = @_;
169 0           require File::Temp;
170              
171 0           my $phrase = PHRASE->[$verbosity];
172 0           my $fill = FILL->[$verbosity];
173 0           my $stroke = STROKE->[$verbosity];
174 0           my $font_size = FONT_SIZE->[$verbosity];
175 0           my ( $x, $y ) = @{ XY->[$verbosity] };
  0            
176 0           my $text = <
177            
178            
181            
182             xmlns:cc="http://creativecommons.org/ns#"
183             xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
184             xmlns:svg="http://www.w3.org/2000/svg"
185             xmlns="http://www.w3.org/2000/svg" width="64px" height="64px"
186             version="1.1">
187            
188            
189             id="rect2993" width="62" height="62" x="1" y="1" />
190            
191             style="font-size:${font_size}px;font-style:normal;font-weight:bold;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#$stroke;fill-opacity:1;stroke:none;font-family:Monospace;opacity:1"
192             x="12.525171" y="28.595528" id="text3763">
193             $phrase
194            
195            
196            
197             END
198 0           my ( $fh, $filename ) =
199             File::Temp::tempfile( TEMPLATE => 'nagXXXXXX', SUFFIX => '.svg' );
200 0           print $fh $text;
201 0           $fh->close;
202 0           return $filename;
203             }
204              
205             1;
206              
207             __END__