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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0297e64a63dc4d2b65d99486da581c29a6358ba608fdcba131264645510fd70
4
- data.tar.gz: d74cd82384fcfbeb340f3047c94e492ab6374d39d9d4e17a75c9e4e35b9429db
3
+ metadata.gz: fd12b179a212d0582bd2740c8697f994f75d71fab58ab9ca28c8330e59e33b08
4
+ data.tar.gz: ab00df9c7f7b5fce00f8a247f346f5049f019d5336973eaec5e17d1970e773da
5
5
  SHA512:
6
- metadata.gz: 2ac7ff052d9007bde1d72c246545d35604293b4e6d60e994cf8c0c668749c694c72595fa61116021457c3a4031c31be70612c32a37ef50dbd17b8e7d85e272c2
7
- data.tar.gz: b062c855186dc5e9624a2aa1a63e625263a7e355ac117b4b09271c377840126c31a4ff9568d71d416c2f4654627b5adadd8963df71089ea24f60a3e515f89b63
6
+ metadata.gz: 5b2a5690bcf64c79fff82138dfeb27063dcc79d65ca29082e387cc396c7787d8beeec4beaf5ab56492bfb1739e1dcabfb2012ebceb34b899b90a118a8cf522b5
7
+ data.tar.gz: d65423b35e47f5287cb44406f618d8cb90825163d1d416e3dc2b1921483fa67ad6e62873c692fd25173a1485d8507a4f70c276a66e2a626cf0d4573e1e5571c8
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- [![Gem Version](https://badge.fury.io/rb/icalpal.svg)](https://badge.fury.io/rb/icalpal)
1
+ [![Gem Version](https://badge.fury.io/rb/icalPal.svg)](https://badge.fury.io/rb/icalPal)
2
2
 
3
- # icalpal
3
+ # icalPal
4
4
 
5
5
  ## Description
6
6
 
7
- icalpal is a command-line tool to query a macOS Calendar and Reminders
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 icalpal
17
+ gem install icalPal
18
18
  ```
19
19
 
20
20
  or in your home diretory:
21
21
 
22
22
  ```
23
- gem install --user-install icalpal
23
+ gem install --user-install icalPal
24
24
  ```
25
25
 
26
26
  As a Homebrew formula:
27
27
 
28
28
  ```
29
- brew tap ajrosen/icalpal
30
- brew install icalpal
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
- icalpal tries to be compatible with icalBuddy for command-line options
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
- ```icalpal accounts```
51
+ ```icalPal accounts```
52
52
 
53
- Shows a list of enabled Calendar accounts. Internally they are known as *Stores*; you can run ```icalpal stores``` instead.
53
+ Shows a list of enabled Calendar accounts. Internally they are known as *Stores*; you can run ```icalPal stores``` instead.
54
54
 
55
- ```icalpal datedTasks```
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., ```icalpal -c ev --da 3``` is the same as ```icalpal -c events --days 3```.
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 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.
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
- icalpal: Usage: icalpal [options] [-c] COMMAND
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
- icalpal supports several output formats. The **default** format tries
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 icalpal.
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,600 lines vs. 7,000. It also means
263
- icalpal is multi-platform.
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 icalpal is written in Ruby and gets its
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, p, m| # Severity, time, progname, msg
31
+ $log.formatter = proc do |s, t, _p, m| # Severity, time, progname, msg
31
32
  ($log.level.positive?)? "#{s}: #{m}\n" :
32
- "[%-5s] %s [%s] - %s\n" %
33
- [ s, t.strftime('%H:%M:%S.%L'), caller(4, 1)[0].split('/')[-1], m ]
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 item === ICalPal::Event && item['sdate']
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::($opts[:cmd])
71
+ klass = ICalPal.call($opts[:cmd])
72
+ success = false
66
73
 
67
74
  # Get it
68
75
  $opts[:db].each do |db|
69
- $log.debug("Trying #{db}")
76
+ $log.debug("Trying #{db}")
70
77
 
71
- if klass == ICalPal::Reminder then
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 { |d| $rows += ICalPal.load_data(d, klass::QUERY) }
75
- else
76
- # Load database
77
- begin
78
- $rows += ICalPal.load_data(db, klass::QUERY)
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'] then
95
- $log.debug(":es")
141
+ if $opts[:es].any? row['account']
142
+ $log.debug(':es')
96
143
  next
97
144
  end
98
145
 
99
- unless $opts[:is].empty? or $opts[:is].any? row['account']
100
- $log.debug(":is");
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 or !row['calendar']
106
- if $opts[:ec].any? row['calendar'] then
107
- $log.debug(":ec")
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? or $opts[:ic].any? row['calendar']
112
- $log.debug(":ic")
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'] then
122
- $log.debug(":et")
168
+ if $opts[:et].any? item['type']
169
+ $log.debug(':et')
123
170
  next
124
171
  end
125
172
 
126
- unless $opts[:it].empty? or $opts[:it].any? item['type']
127
- $log.debug(":it")
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'] then
133
- $log.debug(":el")
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? or $opts[:il].any? item['list_name']
138
- $log.debug(":il")
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)) then
148
- $log.debug(":match")
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? then
157
- $log.debug(":ea")
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? then
162
- $log.debug(":ia")
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 then
167
- $log.debug(":canceled")
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 then
177
- if $opts[:dated] == 1 and item['due_date'] > 0 then
178
- $log.debug(":undated")
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 and item['due_date'] == 0 then
183
- $log.debug(":dated")
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' then
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' then
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' then
253
- xml = items.map { |i| "<#{$opts[:cmd].chomp("s")}>#{i.to_xml}</#{$opts[:cmd].chomp("s")}>" }
254
- "<#{$opts[:cmd]}>\n#{xml.join("")}</#{$opts[:cmd]}>"
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
- "REM #{i['sdate'].strftime('%F AT %R')} " +
258
- "DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i } " +
259
- "MSG #{i['title']}"
260
- }.join("\n")
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.length == 0
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
- unless k.positive?
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