icalPal 3.1.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +19 -19
- data/bin/icalPal +111 -56
- data/bin/icalpal +111 -56
- data/icalPal.gemspec +37 -9
- data/lib/EventKit.rb +24 -18
- data/lib/ToICalPal.rb +28 -26
- data/lib/calendar.rb +1 -2
- data/lib/defaults.rb +8 -7
- data/lib/event.rb +56 -55
- data/lib/icalPal.rb +26 -46
- data/lib/options.rb +40 -36
- data/lib/rdt.rb +7 -5
- data/lib/reminder.rb +22 -22
- data/lib/store.rb +1 -1
- data/lib/utils.rb +45 -0
- data/lib/version.rb +2 -2
- metadata +18 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd12b179a212d0582bd2740c8697f994f75d71fab58ab9ca28c8330e59e33b08
|
4
|
+
data.tar.gz: ab00df9c7f7b5fce00f8a247f346f5049f019d5336973eaec5e17d1970e773da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b2a5690bcf64c79fff82138dfeb27063dcc79d65ca29082e387cc396c7787d8beeec4beaf5ab56492bfb1739e1dcabfb2012ebceb34b899b90a118a8cf522b5
|
7
|
+
data.tar.gz: d65423b35e47f5287cb44406f618d8cb90825163d1d416e3dc2b1921483fa67ad6e62873c692fd25173a1485d8507a4f70c276a66e2a626cf0d4573e1e5571c8
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
[](https://badge.fury.io/rb/icalPal)
|
2
2
|
|
3
|
-
#
|
3
|
+
# icalPal
|
4
4
|
|
5
5
|
## Description
|
6
6
|
|
7
|
-
|
7
|
+
icalPal is a command-line tool to query a macOS Calendar and Reminders
|
8
8
|
databases for accounts, calendars, events, and tasks. It can be run
|
9
9
|
on any system with [Ruby](https://www.ruby-lang.org/) and access to a
|
10
10
|
Calendar or Reminders database.
|
@@ -14,27 +14,27 @@ Calendar or Reminders database.
|
|
14
14
|
As a system-wide Ruby gem:
|
15
15
|
|
16
16
|
```
|
17
|
-
gem install
|
17
|
+
gem install icalPal
|
18
18
|
```
|
19
19
|
|
20
20
|
or in your home diretory:
|
21
21
|
|
22
22
|
```
|
23
|
-
gem install --user-install
|
23
|
+
gem install --user-install icalPal
|
24
24
|
```
|
25
25
|
|
26
26
|
As a Homebrew formula:
|
27
27
|
|
28
28
|
```
|
29
|
-
brew tap ajrosen/
|
30
|
-
brew install
|
29
|
+
brew tap ajrosen/icalPal
|
30
|
+
brew install icalPal
|
31
31
|
```
|
32
32
|
|
33
33
|
## Features
|
34
34
|
|
35
35
|
### Compatability with [icalBuddy](https://github.com/ali-rantakari/icalBuddy)
|
36
36
|
|
37
|
-
|
37
|
+
icalPal tries to be compatible with icalBuddy for command-line options
|
38
38
|
and for output. There are a some important differences to be aware
|
39
39
|
of.
|
40
40
|
|
@@ -48,17 +48,17 @@ of.
|
|
48
48
|
|
49
49
|
### Additional commands
|
50
50
|
|
51
|
-
```
|
51
|
+
```icalPal accounts```
|
52
52
|
|
53
|
-
Shows a list of enabled Calendar accounts. Internally they are known as *Stores*; you can run ```
|
53
|
+
Shows a list of enabled Calendar accounts. Internally they are known as *Stores*; you can run ```icalPal stores``` instead.
|
54
54
|
|
55
|
-
```
|
55
|
+
```icalPal datedTasks```
|
56
56
|
|
57
57
|
Shows only reminders that have a due date.
|
58
58
|
|
59
59
|
### Additional options
|
60
60
|
|
61
|
-
* Options can be abbreviated, so long as they are unique. Eg., ```
|
61
|
+
* Options can be abbreviated, so long as they are unique. Eg., ```icalPal -c ev --da 3``` is the same as ```icalPal -c events --days 3```.
|
62
62
|
* The ```-c``` part is optional, but you cannot abbreviate the command if you leave it off.
|
63
63
|
* Use ```-o``` to print the output in different formats. CSV or JSON are intertesting choices.
|
64
64
|
* Copy your Calendar database file and use ```--db``` on it.
|
@@ -70,11 +70,11 @@ Shows only reminders that have a due date.
|
|
70
70
|
* ```--color``` uses a wider color palette. Calendar colors are what you have chosen in the Calendar app. Not supported in all terminals, but looks great in [iTerm2](https://iterm2.com/).
|
71
71
|
* ```--match``` lets you filter the results of any command to items where a *FIELD* matches a regular expression. Eg., ```--match notes=zoom.us``` to show only Zoom meeetings
|
72
72
|
|
73
|
-
Because
|
73
|
+
Because icalPal is written in Ruby, and not a native Mac application, you can run it just about anywhere. It's been tested with the version of Ruby (2.6.10) included with macOS.
|
74
74
|
|
75
75
|
## Usage
|
76
76
|
|
77
|
-
|
77
|
+
icalPal: Usage: icalPal [options] [-c] COMMAND
|
78
78
|
|
79
79
|
COMMAND must be one of the following:
|
80
80
|
```
|
@@ -205,14 +205,14 @@ Environment variables:
|
|
205
205
|
|
206
206
|
## Output formats
|
207
207
|
|
208
|
-
|
208
|
+
icalPal supports several output formats. The **default** format tries
|
209
209
|
to mimic icalBuddy as much as possible.
|
210
210
|
|
211
211
|
CSV, Hash, JSON, XML, and YAML print all fields for all items in their
|
212
212
|
respective formats. From that you can analyze the results any way you
|
213
213
|
like.
|
214
214
|
|
215
|
-
[Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built in
|
215
|
+
[Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built in icalPal.
|
216
216
|
|
217
217
|
Other formats such as ANSI, HTML, Markdown, RDoc, and TOC, use Ruby's
|
218
218
|
[RDoc::Markup](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup.html)
|
@@ -259,10 +259,10 @@ Lawton](https://github.com/jimlawton) that it even compiles anymore.
|
|
259
259
|
|
260
260
|
Instead of trying to understand and extend the existing code, I chose
|
261
261
|
to start anew using my language of choice: Ruby. Using Ruby meant
|
262
|
-
there is *much* less code; about 1,
|
263
|
-
|
262
|
+
there is *much* less code; about 1,800 lines vs. 7,000. It also means
|
263
|
+
icalPal is multi-platform.
|
264
264
|
|
265
265
|
I won't pretend to understand **why** you would want to run this on
|
266
|
-
Linux or Windows. But since
|
266
|
+
Linux or Windows. But since icalPal is written in Ruby and gets its
|
267
267
|
data directly from the Calendar and Reminders database files instead
|
268
268
|
of an API, you *can*.
|
data/bin/icalPal
CHANGED
@@ -11,6 +11,7 @@ begin
|
|
11
11
|
|
12
12
|
require_relative '../lib/icalpal'
|
13
13
|
require_relative '../lib/options'
|
14
|
+
require_relative '../lib/utils'
|
14
15
|
rescue LoadError => e
|
15
16
|
dep = e.message[/-- (.*)/, 1]
|
16
17
|
|
@@ -27,10 +28,15 @@ end
|
|
27
28
|
|
28
29
|
# All kids love log!
|
29
30
|
$log = Logger.new(STDERR, { level: $defaults[:common][:debug] })
|
30
|
-
$log.formatter = proc do |s, t,
|
31
|
+
$log.formatter = proc do |s, t, _p, m| # Severity, time, progname, msg
|
31
32
|
($log.level.positive?)? "#{s}: #{m}\n" :
|
32
|
-
"[%-
|
33
|
-
|
33
|
+
format("[%-5<sev>s] %<time>s [%<caller>s] - %<message>s\n",
|
34
|
+
{
|
35
|
+
sev: s,
|
36
|
+
time: t.strftime('%H:%M:%S.%L'),
|
37
|
+
caller: caller(4, 1)[0].split('/')[-1],
|
38
|
+
message: m
|
39
|
+
})
|
34
40
|
end
|
35
41
|
|
36
42
|
$opts = ICalPal::Options.new.parse_options
|
@@ -53,7 +59,7 @@ $log.info("Options: #{$opts}")
|
|
53
59
|
def add(item)
|
54
60
|
$log.info("Adding #{item.inspect} #{item['UUID']} (#{item['title']})") if item['UUID']
|
55
61
|
|
56
|
-
item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if
|
62
|
+
item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if ICalPal::Event === item && item['sdate']
|
57
63
|
$items.push(item)
|
58
64
|
end
|
59
65
|
|
@@ -62,22 +68,63 @@ end
|
|
62
68
|
# Load the data
|
63
69
|
|
64
70
|
# What are we getting?
|
65
|
-
klass = ICalPal
|
71
|
+
klass = ICalPal.call($opts[:cmd])
|
72
|
+
success = false
|
66
73
|
|
67
74
|
# Get it
|
68
75
|
$opts[:db].each do |db|
|
69
|
-
|
76
|
+
$log.debug("Trying #{db}")
|
70
77
|
|
71
|
-
|
78
|
+
if klass == ICalPal::Reminder
|
79
|
+
begin
|
72
80
|
# Load all .sqlite files
|
73
81
|
$log.debug("Loading *.sqlite in #{db}")
|
74
|
-
Dir.glob("#{db}/*.sqlite").each
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
82
|
+
Dir.glob("#{db}/*.sqlite").each do |d|
|
83
|
+
$rows += ICalPal.load_data(d, klass::QUERY)
|
84
|
+
success = true
|
85
|
+
|
86
|
+
rescue SQLite3::CantOpenException
|
87
|
+
# Non-fatal exception, try the next one
|
79
88
|
end
|
89
|
+
end
|
90
|
+
else
|
91
|
+
# Load database
|
92
|
+
begin
|
93
|
+
$rows += ICalPal.load_data(db, klass::QUERY)
|
94
|
+
success = true
|
95
|
+
|
96
|
+
rescue SQLite3::CantOpenException
|
97
|
+
# Non-fatal exception, try the next one
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Make sure we opened at least one database
|
103
|
+
unless success
|
104
|
+
$log.fatal('Could not open database')
|
105
|
+
|
106
|
+
# SQLite3 does not return useful error messages. If any databases
|
107
|
+
# failed because of EPERM (operation not permitted), our parent
|
108
|
+
# process might need Full Disk Access, and we should suggest that.
|
109
|
+
eperm = 0
|
110
|
+
|
111
|
+
$opts[:db].each do |db|
|
112
|
+
# Use a real open to get a useful error
|
113
|
+
File.open(db).close
|
114
|
+
rescue Exception => e
|
115
|
+
$log.fatal("#{e.class}: #{db}")
|
116
|
+
|
117
|
+
eperm = 1 if e.instance_of?(Errno::EPERM)
|
80
118
|
end
|
119
|
+
|
120
|
+
if eperm.positive?
|
121
|
+
$stderr.puts
|
122
|
+
$stderr.puts "Does #{ancestor} have Full Disk Access in System Settings?"
|
123
|
+
$stderr.puts
|
124
|
+
$stderr.puts "Try running: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles'"
|
125
|
+
end
|
126
|
+
|
127
|
+
abort
|
81
128
|
end
|
82
129
|
|
83
130
|
$log.info("Loaded #{$rows.count} #{klass} rows")
|
@@ -91,25 +138,25 @@ $rows.each_with_index do |row, i|
|
|
91
138
|
$log.debug("Row #{i}: #{row['ROWID']}:#{row['UUID']} - #{row['account']}/#{row['calendar']}/#{row['title']}")
|
92
139
|
|
93
140
|
# --es/--is
|
94
|
-
if $opts[:es].any? row['account']
|
95
|
-
$log.debug(
|
141
|
+
if $opts[:es].any? row['account']
|
142
|
+
$log.debug(':es')
|
96
143
|
next
|
97
144
|
end
|
98
145
|
|
99
|
-
unless $opts[:is].empty?
|
100
|
-
$log.debug(
|
146
|
+
unless $opts[:is].empty? || ($opts[:is].any? row['account'])
|
147
|
+
$log.debug(':is')
|
101
148
|
next
|
102
149
|
end
|
103
150
|
|
104
151
|
# --ec/--ic
|
105
|
-
unless klass == ICalPal::Store
|
106
|
-
if $opts[:ec].any? row['calendar']
|
107
|
-
$log.debug(
|
152
|
+
unless klass == ICalPal::Store || !row['calendar']
|
153
|
+
if $opts[:ec].any? row['calendar']
|
154
|
+
$log.debug(':ec')
|
108
155
|
next
|
109
156
|
end
|
110
157
|
|
111
|
-
unless $opts[:ic].empty?
|
112
|
-
$log.debug(
|
158
|
+
unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
|
159
|
+
$log.debug(':ic')
|
113
160
|
next
|
114
161
|
end
|
115
162
|
end
|
@@ -118,24 +165,24 @@ $rows.each_with_index do |row, i|
|
|
118
165
|
item = klass.new(row)
|
119
166
|
|
120
167
|
# --et/--it
|
121
|
-
if $opts[:et].any? item['type']
|
122
|
-
$log.debug(
|
168
|
+
if $opts[:et].any? item['type']
|
169
|
+
$log.debug(':et')
|
123
170
|
next
|
124
171
|
end
|
125
172
|
|
126
|
-
unless $opts[:it].empty?
|
127
|
-
$log.debug(
|
173
|
+
unless $opts[:it].empty? || ($opts[:it].any? item['type'])
|
174
|
+
$log.debug(':it')
|
128
175
|
next
|
129
176
|
end
|
130
177
|
|
131
178
|
# --el/--il
|
132
|
-
if $opts[:el].any? item['list_name']
|
133
|
-
$log.debug(
|
179
|
+
if $opts[:el].any? item['list_name']
|
180
|
+
$log.debug(':el')
|
134
181
|
next
|
135
182
|
end
|
136
183
|
|
137
|
-
unless $opts[:il].empty?
|
138
|
-
$log.debug(
|
184
|
+
unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
|
185
|
+
$log.debug(':il')
|
139
186
|
next
|
140
187
|
end
|
141
188
|
|
@@ -144,8 +191,8 @@ $rows.each_with_index do |row, i|
|
|
144
191
|
r = $opts[:match].split('=')
|
145
192
|
|
146
193
|
if item[r[0]].to_s.respond_to?(:match)
|
147
|
-
unless item[r[0]].to_s.match(Regexp.new(r[1].to_s, Regexp::IGNORECASE))
|
148
|
-
$log.debug(
|
194
|
+
unless item[r[0]].to_s.match(Regexp.new(r[1].to_s, Regexp::IGNORECASE))
|
195
|
+
$log.debug(':match')
|
149
196
|
next
|
150
197
|
end
|
151
198
|
end
|
@@ -153,18 +200,18 @@ $rows.each_with_index do |row, i|
|
|
153
200
|
|
154
201
|
if ICalPal::Event === item
|
155
202
|
# Check for all-day and cancelled events
|
156
|
-
if $opts[:ea] && item['all_day'].positive?
|
157
|
-
$log.debug(
|
203
|
+
if $opts[:ea] && item['all_day'].positive?
|
204
|
+
$log.debug(':ea')
|
158
205
|
next
|
159
206
|
end
|
160
207
|
|
161
|
-
if $opts[:ia] && !item['all_day'].positive?
|
162
|
-
$log.debug(
|
208
|
+
if $opts[:ia] && !item['all_day'].positive?
|
209
|
+
$log.debug(':ia')
|
163
210
|
next
|
164
211
|
end
|
165
212
|
|
166
|
-
if item['status'] == :canceled
|
167
|
-
$log.debug(
|
213
|
+
if item['status'] == :canceled
|
214
|
+
$log.debug(':canceled')
|
168
215
|
next
|
169
216
|
end
|
170
217
|
|
@@ -173,14 +220,14 @@ $rows.each_with_index do |row, i|
|
|
173
220
|
item.non_recurring.each { |i| add(i) }
|
174
221
|
else
|
175
222
|
# Check for dated reminders
|
176
|
-
if ICalPal::Reminder === item
|
177
|
-
if $opts[:dated] == 1
|
178
|
-
$log.debug(
|
223
|
+
if ICalPal::Reminder === item
|
224
|
+
if $opts[:dated] == 1 && item['due_date'].positive?
|
225
|
+
$log.debug(':undated')
|
179
226
|
next
|
180
227
|
end
|
181
228
|
|
182
|
-
if $opts[:dated] == 2
|
183
|
-
$log.debug(
|
229
|
+
if $opts[:dated] == 2 && item['due_date'].zero?
|
230
|
+
$log.debug(':dated')
|
184
231
|
next
|
185
232
|
end
|
186
233
|
end
|
@@ -216,7 +263,7 @@ $log.debug("#{$items.count} items remain")
|
|
216
263
|
mu = case $opts[:output]
|
217
264
|
when 'ansi' then RDoc::Markup::ToAnsi.new
|
218
265
|
when 'default' then RDoc::Markup::ToICalPal.new($opts)
|
219
|
-
when 'html'
|
266
|
+
when 'html'
|
220
267
|
rdoc = RDoc::Options.new
|
221
268
|
rdoc.pipe = true
|
222
269
|
rdoc.output_decoration = false
|
@@ -236,7 +283,7 @@ unless mu
|
|
236
283
|
$log.debug("Output in #{$opts[:output]} format")
|
237
284
|
|
238
285
|
puts case $opts[:output]
|
239
|
-
when 'csv'
|
286
|
+
when 'csv'
|
240
287
|
# Get all headers
|
241
288
|
headers = []
|
242
289
|
items.each { |i| headers += i.keys }
|
@@ -249,15 +296,15 @@ unless mu
|
|
249
296
|
table
|
250
297
|
when 'hash' then items.map { |i| i.self }
|
251
298
|
when 'json' then items.map { |i| i.self }.to_json
|
252
|
-
when 'xml'
|
253
|
-
xml = items.map { |i| "<#{$opts[:cmd].chomp(
|
254
|
-
"<#{$opts[:cmd]}>\n#{xml.join(
|
299
|
+
when 'xml'
|
300
|
+
xml = items.map { |i| "<#{$opts[:cmd].chomp('s')}>#{i.to_xml}</#{$opts[:cmd].chomp('s')}>" }
|
301
|
+
"<#{$opts[:cmd]}>\n#{xml.join('')}</#{$opts[:cmd]}>"
|
255
302
|
when 'yaml' then items.map { |i| i.self }.to_yaml
|
256
303
|
when 'remind' then items.map { |i|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
304
|
+
"REM #{i['sdate'].strftime('%F AT %R')} " +
|
305
|
+
"DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i} " +
|
306
|
+
"MSG #{i['title']}"
|
307
|
+
}.join("\n")
|
261
308
|
else abort "No formatter for #{$opts[:output]}"
|
262
309
|
end
|
263
310
|
|
@@ -302,20 +349,28 @@ items.each_with_index do |i, j|
|
|
302
349
|
|
303
350
|
next unless value
|
304
351
|
next if Array === value && !value[0]
|
305
|
-
next if String === value && value.
|
352
|
+
next if String === value && value.empty?
|
306
353
|
|
307
354
|
$log.debug("#{prop}: #{value}")
|
308
355
|
|
309
356
|
# Use Raw to save the property
|
310
357
|
props << RDoc::Markup::Raw.new(prop)
|
311
358
|
|
312
|
-
|
359
|
+
if k.positive?
|
360
|
+
props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
|
361
|
+
props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless (i['placeholder'])
|
362
|
+
else
|
313
363
|
# First property, value only
|
314
364
|
props << RDoc::Markup::Heading.new(2, value.to_s)
|
315
|
-
else
|
316
|
-
props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
|
317
|
-
props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless(i['placeholder'])
|
318
365
|
end
|
366
|
+
|
367
|
+
# unless k.positive?
|
368
|
+
# # First property, value only
|
369
|
+
# props << RDoc::Markup::Heading.new(2, value.to_s)
|
370
|
+
# else
|
371
|
+
# props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
|
372
|
+
# props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless (i['placeholder'])
|
373
|
+
# end
|
319
374
|
end
|
320
375
|
|
321
376
|
# Print it
|