| 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__ |