File Coverage

blib/lib/App/Greple/ical.pm
Criterion Covered Total %
statement 14 50 28.0
branch 0 24 0.0
condition 0 12 0.0
subroutine 5 9 55.5
pod 0 4 0.0
total 19 99 19.1


line stmt bran cond sub pod time code
1             =encoding utf-8
2              
3             =head1 NAME
4              
5             ical - Module to support Apple macOS Calendar data
6              
7             =head1 SYNOPSIS
8              
9             greple -Mical [ options ]
10              
11             --simple print data in one line
12             --detail print one line data with description if available
13              
14             =head1 SAMPLES
15              
16             greple -Mical PATTERN
17              
18             greple -Mical --simple PATTERN
19              
20             greple -Mical --detail PATTERN
21              
22             =head1 DESCRIPTION
23              
24             This module searches Apple macOS Calendar data.
25              
26             Recent versions of macOS store calendar data in a SQLite database
27             (F under F<~/Library/Group Containers/group.com.apple.calendar>),
28             instead of individual C<.ics> files which older versions used. This
29             module reads the database with the B command and converts
30             each event to a C-like paragraph, which is then searched by
31             B in paragraph mode:
32              
33             BEGIN:VEVENT
34             DTSTART:20260903T163000
35             DTEND:20260903T190000
36             SUMMARY:映画:ローマの休日
37             LOCATION:Theater X
38             END:VEVENT
39              
40             Used without options, matched events are printed in the above format.
41              
42             With B<--simple> option, summarize content in single line:
43              
44             2026/09/03 16:30-19:00 映画:ローマの休日 @[Theater X]
45              
46             With B<--detail> option, print summarized line with description data
47             if it is attached. The result is sorted.
48              
49             =head1 REQUIREMENTS
50              
51             The B command is required (standard on macOS).
52              
53             The terminal application needs the B privilege to
54             read the calendar database. If you get an "Operation not permitted"
55             error, add your terminal application in: System Settings ->
56             Privacy & Security -> Full Disk Access, and restart the terminal.
57              
58             =head1 TIPS
59              
60             Use C<-dfn> option to observe the command running status.
61              
62             Use C<-ds> option to see statistics information.
63              
64             =head1 SEE ALSO
65              
66             RFC2445
67              
68             =head1 AUTHOR
69              
70             Kazumasa Utashiro
71              
72             =head1 LICENSE
73              
74             Copyright 2017-2026 Kazumasa Utashiro.
75              
76             This library is free software; you can redistribute it and/or modify
77             it under the same terms as Perl itself.
78              
79             =cut
80              
81             package App::Greple::ical;
82              
83             our $VERSION = '1.00';
84              
85 1     1   202289 use v5.14;
  1         3  
86 1     1   4 use warnings;
  1         1  
  1         37  
87 1     1   5 use Carp;
  1         2  
  1         70  
88 1     1   4 use Exporter 'import';
  1         1  
  1         22  
89              
90 1     1   480 use App::Greple::Common qw(FILELABEL);
  1         318  
  1         656  
91              
92             our @EXPORT = qw(&print_simple &print_detail &print_desc &ical_data);
93              
94             ##
95             ## Convert Calendar.sqlitedb to VEVENT-like paragraphs.
96             ## Dates in the database are in Core Data epoch (seconds since
97             ## 2001-01-01 UTC); 978307200 is the offset to Unix epoch.
98             ##
99             my $SQL = <<'END';
100             SELECT 'BEGIN:VEVENT' || char(10)
101             || 'DTSTART:' || strftime(CASE WHEN i.all_day THEN '%Y%m%d' ELSE '%Y%m%dT%H%M%S' END,
102             i.start_date + 978307200, 'unixepoch', 'localtime') || char(10)
103             || CASE WHEN i.end_date IS NOT NULL
104             THEN 'DTEND:' || strftime(CASE WHEN i.all_day THEN '%Y%m%d' ELSE '%Y%m%dT%H%M%S' END,
105             i.end_date + 978307200, 'unixepoch', 'localtime') || char(10)
106             ELSE '' END
107             || 'SUMMARY:' || replace(i.summary, char(10), ' ') || char(10)
108             || CASE WHEN loc.title IS NOT NULL
109             THEN 'LOCATION:' || replace(loc.title, char(10), ' ') || char(10)
110             ELSE '' END
111             || CASE WHEN i.description IS NOT NULL
112             THEN 'DESCRIPTION:' || replace(i.description, char(10), '\n') || char(10)
113             ELSE '' END
114             || 'END:VEVENT' || char(10)
115             FROM CalendarItem i
116             LEFT JOIN Location loc ON i.location_id = loc.ROWID
117             WHERE i.summary IS NOT NULL AND i.start_date IS NOT NULL
118             ORDER BY i.start_date
119             END
120              
121             ##
122             ## Input filter function. Called with FILELABEL parameter, and
123             ## responsible to replace STDIN by the filtered stream (see
124             ## App::Greple::Filter).
125             ##
126             sub ical_data {
127 0     0 0   my %arg = @_;
128 0   0       my $file = $arg{&FILELABEL} // croak "no filename";
129 0   0       my $pid = open(STDIN, '-|') // croak "process fork failed";
130 0 0         if ($pid == 0) {
131 0 0         exec 'sqlite3', '-noheader', $file, $SQL or die "sqlite3: $!\n";
132             }
133 0           $pid;
134             }
135              
136             sub print_detail {
137 0     0 0   $_ = &print_simple . &print_desc . "\n";
138 0           s/\n(?=.)/\r/sg;
139 0           $_;
140             }
141              
142             sub print_simple {
143 0     0 0   s/\r//g;
144 0           my $s = '';
145 0           my(@s, @e);
146 0 0         if (@s = /^DTSTART.*(\d{4})(\d\d)(\d\d)(?:T(\d\d)(\d\d))?/m) {
147 0           $s .= "$1/$2/$3";
148 0 0         $s .= " $4:$5" if defined $4;
149             }
150 0 0         if (@e = /^DTEND.*(\d{4})(\d\d)(\d\d)(?:T(\d\d)(\d\d))?/m) {
151 0 0 0       if ($s[0]eq$e[0] and $s[1]eq$e[1] and $s[2]+1>=$e[2]) {
      0        
152 0 0         $s .= "-$4:$5" if defined $4;
153             } else {
154 0           $s .= "-";
155 0 0         $s .= "$1/" if $s[0] ne $e[0];
156 0           $s .= "$2/$3";
157             }
158             }
159 0           $s .= " ";
160 0 0         /^SUMMARY:(.*)/m and $s .= $1;
161 0 0         /^DESCRIPTION:/m and $s .= "*";
162 0 0         /^LOCATION:(.*)/m and $s .= " \@[$1]";
163 0           $s .= "\n";
164 0           $s;
165             }
166              
167             sub print_desc {
168 0     0 0   my $desc = '';
169 0 0         if (/^(DESCRIPTION.*\n(?:\s.*\n)*)/m) {
170 0           $desc = $1;
171 0           for ($desc) {
172 0           s/\n\s+//g;
173 0           s/\\n/\n/g;
174 0           s/\\\\t/\t/g;
175 0           s/\\,/,/g;
176             }
177             }
178 0           $desc;
179             }
180              
181             1;
182              
183             __DATA__