icalPal 3.3.0 → 3.4.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: fd12b179a212d0582bd2740c8697f994f75d71fab58ab9ca28c8330e59e33b08
4
- data.tar.gz: ab00df9c7f7b5fce00f8a247f346f5049f019d5336973eaec5e17d1970e773da
3
+ metadata.gz: 3a74d33ac1b8d65466f6ef4820813ad983c7bb539438c11d12eb61daaa2af83f
4
+ data.tar.gz: 3c2790143a7b5abde6304312099988de264cc767fde30c34dd8b7682a80bd782
5
5
  SHA512:
6
- metadata.gz: 5b2a5690bcf64c79fff82138dfeb27063dcc79d65ca29082e387cc396c7787d8beeec4beaf5ab56492bfb1739e1dcabfb2012ebceb34b899b90a118a8cf522b5
7
- data.tar.gz: d65423b35e47f5287cb44406f618d8cb90825163d1d416e3dc2b1921483fa67ad6e62873c692fd25173a1485d8507a4f70c276a66e2a626cf0d4573e1e5571c8
6
+ metadata.gz: 031fae41c01699e5eb757dd0f838e702fccd0d65fb3ab1aca3a6dde20083ed5ec43c88a1bc2c43e11e0a77399aea21e860d3f13f7590d90a00f912d3d87bff1b
7
+ data.tar.gz: 9e1cfa76f56f735406553d40d13e16ce786461ab331eea9cc0757ffe0858e92ef3c9bae843934f9a55098ec1a28d6e03852aed3ce3bc244a56de362b584de5bd
data/README.md CHANGED
@@ -4,11 +4,27 @@
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 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.
11
11
 
12
+ <!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
13
+
14
+ **Table of Contents**
15
+
16
+ - [Installation](#installation)
17
+ - [Features](#features)
18
+ - [Compatability with icalBuddy](#compatability-with-icalbuddy)
19
+ - [Additional commands](#additional-commands)
20
+ - [Additional options](#additional-options)
21
+ - [Usage](#usage)
22
+ - [Output formats](#output-formats)
23
+ - [History](#history)
24
+
25
+ <!-- markdown-toc end -->
26
+
27
+
12
28
  ## Installation
13
29
 
14
30
  As a system-wide Ruby gem:
@@ -23,18 +39,11 @@ or in your home diretory:
23
39
  gem install --user-install icalPal
24
40
  ```
25
41
 
26
- As a Homebrew formula:
27
-
28
- ```
29
- brew tap ajrosen/icalPal
30
- brew install icalPal
31
- ```
32
-
33
42
  ## Features
34
43
 
35
- ### Compatability with [icalBuddy](https://github.com/ali-rantakari/icalBuddy)
44
+ ### Compatability with icalBuddy
36
45
 
37
- icalPal tries to be compatible with icalBuddy for command-line options
46
+ icalPal tries to be compatible with [icalBuddy](https://github.com/ali-rantakari/icalBuddy) for command-line options
38
47
  and for output. There are a some important differences to be aware
39
48
  of.
40
49
 
@@ -62,7 +71,7 @@ Shows only reminders that have a due date.
62
71
  * The ```-c``` part is optional, but you cannot abbreviate the command if you leave it off.
63
72
  * Use ```-o``` to print the output in different formats. CSV or JSON are intertesting choices.
64
73
  * Copy your Calendar database file and use ```--db``` on it.
65
- * ```--it``` and ```--et``` will filter by Calendar *type*. Types are **Local**, **Exchange**, **CalDAV**, **MobileMe**, **Subscribed**, and **Birthdays**
74
+ * ```--it``` and ```--et``` will filter by Calendar *type*. Types are **Local**, **Exchange**, **CalDAV**, **MobileMe**, **Subscribed**, **Birthdays**, and **Reminders**
66
75
  * ```--il``` and ```-el``` will filter by Reminder list
67
76
  * ```--ia``` includes *only* all-day events (opposite of ```--ea```)
68
77
  * ```--aep``` is like ```--iep```, but *adds* to the default property list instead of replacing it.
@@ -201,6 +210,8 @@ Environment variables:
201
210
  ICALPAL Additional arguments
202
211
  ICALPAL_CONFIG Additional arguments from a file
203
212
  (default: /Users/ajr/.icalpal)
213
+
214
+ Do not quote or escape values. Options set in ICALPAL override ICALPAL_CONFIG. Options on the command line override ICALPAL.
204
215
  ```
205
216
 
206
217
  ## Output formats
@@ -212,7 +223,7 @@ CSV, Hash, JSON, XML, and YAML print all fields for all items in their
212
223
  respective formats. From that you can analyze the results any way you
213
224
  like.
214
225
 
215
- [Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built in icalPal.
226
+ [Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built into icalPal.
216
227
 
217
228
  Other formats such as ANSI, HTML, Markdown, RDoc, and TOC, use Ruby's
218
229
  [RDoc::Markup](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup.html)
data/bin/icalPal CHANGED
@@ -29,14 +29,14 @@ end
29
29
  # All kids love log!
30
30
  $log = Logger.new(STDERR, { level: $defaults[:common][:debug] })
31
31
  $log.formatter = proc do |s, t, _p, m| # Severity, time, progname, msg
32
- ($log.level.positive?)? "#{s}: #{m}\n" :
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
- })
32
+ format("[%-5<sev>s] %<time>s [%<file>s:%<line>5s] - %<message>s\n",
33
+ {
34
+ sev: s,
35
+ time: t.strftime('%H:%M:%S.%L'),
36
+ file: caller(4, 1)[0].split('/')[-1].split(':')[0],
37
+ line: caller(4, 1)[0].split('/')[-1].split(':')[1],
38
+ message: m
39
+ })
40
40
  end
41
41
 
42
42
  $opts = ICalPal::Options.new.parse_options
@@ -57,9 +57,8 @@ $log.info("Options: #{$opts}")
57
57
  # @param item[Object]
58
58
 
59
59
  def add(item)
60
- $log.info("Adding #{item.inspect} #{item['UUID']} (#{item['title']})") if item['UUID']
60
+ $log.debug("Adding #{item.dump} #{item['UUID']} (#{item['title']})") if item['UUID']
61
61
 
62
- item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if ICalPal::Event === item && item['sdate']
63
62
  $items.push(item)
64
63
  end
65
64
 
@@ -128,108 +127,58 @@ unless success
128
127
  end
129
128
 
130
129
  $log.info("Loaded #{$rows.count} #{klass} rows")
130
+ $log.info("Window is #{$opts[:from]} to #{$opts[:to]}")
131
131
 
132
132
 
133
133
  ##################################################
134
134
  # Process the data
135
135
 
136
136
  # Add rows
137
- $rows.each_with_index do |row, i|
138
- $log.debug("Row #{i}: #{row['ROWID']}:#{row['UUID']} - #{row['account']}/#{row['calendar']}/#{row['title']}")
139
-
137
+ $rows.each do |row|
140
138
  # --es/--is
141
- if $opts[:es].any? row['account']
142
- $log.debug(':es')
143
- next
144
- end
145
-
146
- unless $opts[:is].empty? || ($opts[:is].any? row['account'])
147
- $log.debug(':is')
148
- next
149
- end
139
+ next if $opts[:es].any? row['account']
140
+ next unless $opts[:is].empty? || ($opts[:is].any? row['account'])
150
141
 
151
142
  # --ec/--ic
152
143
  unless klass == ICalPal::Store || !row['calendar']
153
- if $opts[:ec].any? row['calendar']
154
- $log.debug(':ec')
155
- next
156
- end
157
-
158
- unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
159
- $log.debug(':ic')
160
- next
161
- end
144
+ next if $opts[:ec].any? row['calendar']
145
+ next unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
162
146
  end
163
147
 
164
148
  # Instantiate an item
165
149
  item = klass.new(row)
166
150
 
167
151
  # --et/--it
168
- if $opts[:et].any? item['type']
169
- $log.debug(':et')
170
- next
171
- end
172
-
173
- unless $opts[:it].empty? || ($opts[:it].any? item['type'])
174
- $log.debug(':it')
175
- next
176
- end
152
+ next if $opts[:et].any? item['type']
153
+ next unless $opts[:it].empty? || ($opts[:it].any? item['type'])
177
154
 
178
155
  # --el/--il
179
- if $opts[:el].any? item['list_name']
180
- $log.debug(':el')
181
- next
182
- end
183
-
184
- unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
185
- $log.debug(':il')
186
- next
187
- end
156
+ next if $opts[:el].any? item['list_name']
157
+ next unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
188
158
 
189
159
  # --match
190
160
  if $opts[:match]
191
161
  r = $opts[:match].split('=')
192
162
 
193
163
  if item[r[0]].to_s.respond_to?(:match)
194
- unless item[r[0]].to_s.match(Regexp.new(r[1].to_s, Regexp::IGNORECASE))
195
- $log.debug(':match')
196
- next
197
- end
164
+ next unless item[r[0]].to_s =~ Regexp.new(r[1], Regexp::IGNORECASE)
198
165
  end
199
166
  end
200
167
 
201
168
  if ICalPal::Event === item
202
169
  # Check for all-day and cancelled events
203
- if $opts[:ea] && item['all_day'].positive?
204
- $log.debug(':ea')
205
- next
206
- end
207
-
208
- if $opts[:ia] && !item['all_day'].positive?
209
- $log.debug(':ia')
210
- next
211
- end
212
-
213
- if item['status'] == :canceled
214
- $log.debug(':canceled')
215
- next
216
- end
170
+ next if $opts[:ea] && item['all_day'].positive?
171
+ next if $opts[:ia] && !item['all_day'].positive?
172
+ next if item['status'] == :canceled
217
173
 
218
174
  (item['has_recurrences'].positive?)?
219
- item.recurring.each { |i| add(i) } :
220
- item.non_recurring.each { |i| add(i) }
175
+ item.recurring.each { |j| add(j) } :
176
+ item.non_recurring.each { |j| add(j) }
221
177
  else
222
178
  # Check for dated reminders
223
179
  if ICalPal::Reminder === item
224
- if $opts[:dated] == 1 && item['due_date'].positive?
225
- $log.debug(':undated')
226
- next
227
- end
228
-
229
- if $opts[:dated] == 2 && item['due_date'].zero?
230
- $log.debug(':dated')
231
- next
232
- end
180
+ next if $opts[:dated] == 1 && item['due_date'].positive?
181
+ next if $opts[:dated] == 2 && item['due_date'].zero?
233
182
  end
234
183
 
235
184
  add(item)
@@ -248,11 +197,10 @@ end
248
197
 
249
198
  # Sort the rows
250
199
  begin
251
- $log.info("Sorting/uniqing #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
200
+ $log.info("Sorting #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
252
201
 
253
202
  $items.sort_by! { |i| [ i[$opts[:sep]], i[$opts[:sort]], i['sdate'] ] }
254
203
  $items.reverse! if $opts[:reverse]
255
- $items.uniq!
256
204
  rescue Exception => e
257
205
  $log.info("Sorting failed: #{e}\n")
258
206
  end
@@ -298,7 +246,7 @@ unless mu
298
246
  when 'json' then items.map { |i| i.self }.to_json
299
247
  when 'xml'
300
248
  xml = items.map { |i| "<#{$opts[:cmd].chomp('s')}>#{i.to_xml}</#{$opts[:cmd].chomp('s')}>" }
301
- "<#{$opts[:cmd]}>\n#{xml.join('')}</#{$opts[:cmd]}>"
249
+ "<#{$opts[:cmd]}>\n#{xml.join}</#{$opts[:cmd]}>"
302
250
  when 'yaml' then items.map { |i| i.self }.to_yaml
303
251
  when 'remind' then items.map { |i|
304
252
  "REM #{i['sdate'].strftime('%F AT %R')} " +
@@ -363,14 +311,6 @@ items.each_with_index do |i, j|
363
311
  # First property, value only
364
312
  props << RDoc::Markup::Heading.new(2, value.to_s)
365
313
  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
374
314
  end
375
315
 
376
316
  # Print it
data/bin/icalpal CHANGED
@@ -29,14 +29,14 @@ end
29
29
  # All kids love log!
30
30
  $log = Logger.new(STDERR, { level: $defaults[:common][:debug] })
31
31
  $log.formatter = proc do |s, t, _p, m| # Severity, time, progname, msg
32
- ($log.level.positive?)? "#{s}: #{m}\n" :
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
- })
32
+ format("[%-5<sev>s] %<time>s [%<file>s:%<line>5s] - %<message>s\n",
33
+ {
34
+ sev: s,
35
+ time: t.strftime('%H:%M:%S.%L'),
36
+ file: caller(4, 1)[0].split('/')[-1].split(':')[0],
37
+ line: caller(4, 1)[0].split('/')[-1].split(':')[1],
38
+ message: m
39
+ })
40
40
  end
41
41
 
42
42
  $opts = ICalPal::Options.new.parse_options
@@ -57,9 +57,8 @@ $log.info("Options: #{$opts}")
57
57
  # @param item[Object]
58
58
 
59
59
  def add(item)
60
- $log.info("Adding #{item.inspect} #{item['UUID']} (#{item['title']})") if item['UUID']
60
+ $log.debug("Adding #{item.dump} #{item['UUID']} (#{item['title']})") if item['UUID']
61
61
 
62
- item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if ICalPal::Event === item && item['sdate']
63
62
  $items.push(item)
64
63
  end
65
64
 
@@ -128,108 +127,58 @@ unless success
128
127
  end
129
128
 
130
129
  $log.info("Loaded #{$rows.count} #{klass} rows")
130
+ $log.info("Window is #{$opts[:from]} to #{$opts[:to]}")
131
131
 
132
132
 
133
133
  ##################################################
134
134
  # Process the data
135
135
 
136
136
  # Add rows
137
- $rows.each_with_index do |row, i|
138
- $log.debug("Row #{i}: #{row['ROWID']}:#{row['UUID']} - #{row['account']}/#{row['calendar']}/#{row['title']}")
139
-
137
+ $rows.each do |row|
140
138
  # --es/--is
141
- if $opts[:es].any? row['account']
142
- $log.debug(':es')
143
- next
144
- end
145
-
146
- unless $opts[:is].empty? || ($opts[:is].any? row['account'])
147
- $log.debug(':is')
148
- next
149
- end
139
+ next if $opts[:es].any? row['account']
140
+ next unless $opts[:is].empty? || ($opts[:is].any? row['account'])
150
141
 
151
142
  # --ec/--ic
152
143
  unless klass == ICalPal::Store || !row['calendar']
153
- if $opts[:ec].any? row['calendar']
154
- $log.debug(':ec')
155
- next
156
- end
157
-
158
- unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
159
- $log.debug(':ic')
160
- next
161
- end
144
+ next if $opts[:ec].any? row['calendar']
145
+ next unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
162
146
  end
163
147
 
164
148
  # Instantiate an item
165
149
  item = klass.new(row)
166
150
 
167
151
  # --et/--it
168
- if $opts[:et].any? item['type']
169
- $log.debug(':et')
170
- next
171
- end
172
-
173
- unless $opts[:it].empty? || ($opts[:it].any? item['type'])
174
- $log.debug(':it')
175
- next
176
- end
152
+ next if $opts[:et].any? item['type']
153
+ next unless $opts[:it].empty? || ($opts[:it].any? item['type'])
177
154
 
178
155
  # --el/--il
179
- if $opts[:el].any? item['list_name']
180
- $log.debug(':el')
181
- next
182
- end
183
-
184
- unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
185
- $log.debug(':il')
186
- next
187
- end
156
+ next if $opts[:el].any? item['list_name']
157
+ next unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
188
158
 
189
159
  # --match
190
160
  if $opts[:match]
191
161
  r = $opts[:match].split('=')
192
162
 
193
163
  if item[r[0]].to_s.respond_to?(:match)
194
- unless item[r[0]].to_s.match(Regexp.new(r[1].to_s, Regexp::IGNORECASE))
195
- $log.debug(':match')
196
- next
197
- end
164
+ next unless item[r[0]].to_s =~ Regexp.new(r[1], Regexp::IGNORECASE)
198
165
  end
199
166
  end
200
167
 
201
168
  if ICalPal::Event === item
202
169
  # Check for all-day and cancelled events
203
- if $opts[:ea] && item['all_day'].positive?
204
- $log.debug(':ea')
205
- next
206
- end
207
-
208
- if $opts[:ia] && !item['all_day'].positive?
209
- $log.debug(':ia')
210
- next
211
- end
212
-
213
- if item['status'] == :canceled
214
- $log.debug(':canceled')
215
- next
216
- end
170
+ next if $opts[:ea] && item['all_day'].positive?
171
+ next if $opts[:ia] && !item['all_day'].positive?
172
+ next if item['status'] == :canceled
217
173
 
218
174
  (item['has_recurrences'].positive?)?
219
- item.recurring.each { |i| add(i) } :
220
- item.non_recurring.each { |i| add(i) }
175
+ item.recurring.each { |j| add(j) } :
176
+ item.non_recurring.each { |j| add(j) }
221
177
  else
222
178
  # Check for dated reminders
223
179
  if ICalPal::Reminder === item
224
- if $opts[:dated] == 1 && item['due_date'].positive?
225
- $log.debug(':undated')
226
- next
227
- end
228
-
229
- if $opts[:dated] == 2 && item['due_date'].zero?
230
- $log.debug(':dated')
231
- next
232
- end
180
+ next if $opts[:dated] == 1 && item['due_date'].positive?
181
+ next if $opts[:dated] == 2 && item['due_date'].zero?
233
182
  end
234
183
 
235
184
  add(item)
@@ -248,11 +197,10 @@ end
248
197
 
249
198
  # Sort the rows
250
199
  begin
251
- $log.info("Sorting/uniqing #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
200
+ $log.info("Sorting #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
252
201
 
253
202
  $items.sort_by! { |i| [ i[$opts[:sep]], i[$opts[:sort]], i['sdate'] ] }
254
203
  $items.reverse! if $opts[:reverse]
255
- $items.uniq!
256
204
  rescue Exception => e
257
205
  $log.info("Sorting failed: #{e}\n")
258
206
  end
@@ -298,7 +246,7 @@ unless mu
298
246
  when 'json' then items.map { |i| i.self }.to_json
299
247
  when 'xml'
300
248
  xml = items.map { |i| "<#{$opts[:cmd].chomp('s')}>#{i.to_xml}</#{$opts[:cmd].chomp('s')}>" }
301
- "<#{$opts[:cmd]}>\n#{xml.join('')}</#{$opts[:cmd]}>"
249
+ "<#{$opts[:cmd]}>\n#{xml.join}</#{$opts[:cmd]}>"
302
250
  when 'yaml' then items.map { |i| i.self }.to_yaml
303
251
  when 'remind' then items.map { |i|
304
252
  "REM #{i['sdate'].strftime('%F AT %R')} " +
@@ -363,14 +311,6 @@ items.each_with_index do |i, j|
363
311
  # First property, value only
364
312
  props << RDoc::Markup::Heading.new(2, value.to_s)
365
313
  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
374
314
  end
375
315
 
376
316
  # Print it
data/icalPal.gemspec CHANGED
@@ -16,33 +16,35 @@ EOF
16
16
  s.licenses = [ 'GPL-3.0-or-later' ]
17
17
 
18
18
  s.metadata = {
19
- 'bug_tracker_uri' => "https://github.com/ajrosen/#{s.name}/issues"
19
+ 'bug_tracker_uri' => "https://github.com/ajrosen/#{s.name}/issues",
20
+ 'rubygems_mfa_required' => 'true'
20
21
  }
21
22
 
22
23
  s.files = Dir["#{s.name}.gemspec", 'bin/*', 'lib/*.rb']
23
24
  s.executables = [ "#{s.name}" ]
24
25
  s.extra_rdoc_files = [ 'README.md' ]
25
26
 
26
- s.add_dependency 'nokogiri-plist', '~> 0.5.0'
27
- s.add_dependency 'sqlite3', '~> 2.6.0' unless s.rubygems_version == `/usr/bin/gem --version`.strip
28
-
29
27
  # The macOS and Homebrew versions of rubygems have incompatible
30
28
  # requirements for sqlite3.
31
- #
29
+ #
32
30
  # macOS comes with version 1.3.13, so it does not need to be added
33
31
  # as a dependency, but it cannot install anything newer:
34
- #
32
+ #
35
33
  # requires Ruby version >= 3.0, < 3.4.dev. The current ruby version is 2.6.10.
36
- #
34
+ #
37
35
  # Homebrew's Ruby formula does not come with sqlite3, so it does
38
36
  # need to be added as a dependency, but it cannot install version
39
37
  # 1.3.13:
40
- #
38
+ #
41
39
  # error: call to undeclared function
42
- #
40
+ #
43
41
  # So we must call add_dependency, but iff we are not building with
44
42
  # macOS' Ruby installation.
45
43
 
44
+ s.add_dependency 'nokogiri-plist', '~> 0.5.0'
45
+ s.add_dependency 'sqlite3', '~> 2.6.0' unless s.rubygems_version == `/usr/bin/gem --version`.strip
46
+ s.add_dependency 'timezone', '>= 0.99', '~> 1.3.0'
47
+
46
48
  s.bindir = 'bin'
47
49
  s.required_ruby_version = '>= 2.6.0'
48
50
 
data/lib/ToICalPal.rb CHANGED
@@ -6,25 +6,25 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
6
6
  # ANSI[https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-T.416-199303-I!!PDF-E&type=items]
7
7
  # colors
8
8
  ANSI = {
9
- 'black': 30, '#000000': '38;5;0',
10
- 'red': 31, '#ff0000': '38;5;1',
11
- 'green': 32, '#00ff00': '38;5;2',
12
- 'yellow': 33, '#ffff00': '38;5;3',
13
- 'blue': 34, '#0000ff': '38;5;4',
14
- 'magenta': 35, '#ff00ff': '38;5;5',
15
- 'cyan': 36, '#00ffff': '38;5;6',
16
- 'white': 37, '#ffffff': '38;5;255',
17
- 'default': 39, 'custom': nil,
9
+ black: 30, '#000000': '38;5;0',
10
+ red: 31, '#ff0000': '38;5;1',
11
+ green: 32, '#00ff00': '38;5;2',
12
+ yellow: 33, '#ffff00': '38;5;3',
13
+ blue: 34, '#0000ff': '38;5;4',
14
+ magenta: 35, '#ff00ff': '38;5;5',
15
+ cyan: 36, '#00ffff': '38;5;6',
16
+ white: 37, '#ffffff': '38;5;255',
17
+ default: 39, custom: nil,
18
18
 
19
19
  # Reminders custom colors
20
- 'brown': '38;2;162;132;94',
21
- 'gray': '38;2;91;98;106',
22
- 'indigo': '38;2;88;86;214',
23
- 'lightblue': '38;2;90;200;250',
24
- 'orange': '38;2;255;149;0',
25
- 'pink': '38;2;255;45;85',
26
- 'purple': '38;2;204;115;225',
27
- 'rose': '38;2;217;166;159',
20
+ brown: '38;2;162;132;94',
21
+ gray: '38;2;91;98;106',
22
+ indigo: '38;2;88;86;214',
23
+ lightblue: '38;2;90;200;250',
24
+ orange: '38;2;255;149;0',
25
+ pink: '38;2;255;45;85',
26
+ purple: '38;2;204;115;225',
27
+ rose: '38;2;217;166;159',
28
28
  }.freeze
29
29
 
30
30
  # Increased intensity
@@ -55,7 +55,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
55
55
  # @option opts [Array<String>] :ps List of property separators
56
56
  # @option opts [String] :ss Section separator
57
57
  def initialize(opts)
58
- super(opts)
58
+ super
59
59
  @opts = opts
60
60
  end
61
61
 
@@ -76,10 +76,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
76
76
  def accept_list_start(_arg)
77
77
  begin
78
78
  return if @item['placeholder']
79
- rescue
80
- end
81
79
 
82
- begin
83
80
  if (@item['due_date'] + ICalPal::ITIME).between?(ICalPal::ITIME + 1, $now.to_i)
84
81
  @res << "#{@opts[:ab]} " unless @opts[:nb]
85
82
  return
@@ -95,7 +92,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
95
92
  # @param arg [RDoc::Markup::ListItem]
96
93
  # @option arg [String] .label Contains the property name
97
94
  def accept_list_item_start(arg)
98
- @res << @opts[:ps][@ps] || ' ' unless @item['placeholder']
95
+ @res << (@opts[:ps][@ps] || ' ') unless @item['placeholder']
99
96
  @res << colorize(*LABEL_COLOR, arg.label) << ': ' unless @opts[:npn] || NO_LABEL.any?(arg.label)
100
97
 
101
98
  @ps += 1 unless @ps == @opts[:ps].count - 1
@@ -133,7 +130,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
133
130
  # @param p [RDoc::Markup::Paragraph]
134
131
  # @option p [Array<String>] :parts The property's text
135
132
  def accept_paragraph(p)
136
- t = p.parts.join('; ').gsub(/\n/, "\n ")
133
+ t = p.parts.join('; ').gsub("\n", "\n ")
137
134
  t = colorize(*DATE_COLOR, t) if @prop == 'datetime'
138
135
  @res << t
139
136
  end
@@ -196,10 +193,10 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
196
193
 
197
194
  # @!visibility private
198
195
 
199
- # @param a [Array] Ignored
200
- def accept_list_end(a) end
196
+ # @param _a [Array] Ignored
197
+ def accept_list_end(_a) end
201
198
 
202
- # @param a [Array] Ignored
203
- def accept_list_item_end(a) end
199
+ # @param _a [Array] Ignored
200
+ def accept_list_item_end(_a) end
204
201
 
205
202
  end
data/lib/defaults.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # Does anybody really know what time it is?
2
- $now = ICalPal::RDT.now
3
- $today = ICalPal::RDT.new(*$now.to_a[0..2] + [ 0, 0, 0, $now.zone ])
2
+ now = Time.now
3
+ $now = ICalPal::RDT.from_time(now)
4
+ $today = ICalPal::RDT.new(*$now.to_a[0..2] + [ 0, 0, 0 ])
4
5
 
5
6
  # Defaults
6
7
  $defaults = {
@@ -8,11 +9,11 @@ $defaults = {
8
9
  ab: '!',
9
10
  aep: [],
10
11
  bullet: '•',
11
- cf: "#{ENV['HOME']}/.icalpal",
12
+ cf: "#{Dir.home}/.icalpal",
12
13
  color: false,
13
14
  db: [
14
- "#{ENV['HOME']}/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb",
15
- "#{ENV['HOME']}/Library/Calendars/Calendar.sqlitedb",
15
+ "#{Dir.home}/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb",
16
+ "#{Dir.home}/Library/Calendars/Calendar.sqlitedb",
16
17
  ],
17
18
  debug: Logger::WARN,
18
19
  df: '%b %-d, %Y',
@@ -77,7 +78,7 @@ $defaults = {
77
78
  ps: [ "\n " ],
78
79
  sa: false,
79
80
  sed: false,
80
- sort: 'sdate',
81
+ sort: 'sctime',
81
82
  ss: "\n------------------------",
82
83
  to: nil,
83
84
  uid: false,
data/lib/event.rb CHANGED
@@ -1,18 +1,20 @@
1
- require 'time'
1
+ require 'timezone'
2
2
 
3
3
  module ICalPal
4
4
  # Class representing items from the <tt>CalendarItem</tt> table
5
5
  class Event
6
6
  include ICalPal
7
7
 
8
- # Standard accessor with special handling for +sdate+. Setting
9
- # +sdate+ will also set +sday+.
8
+ # Standard accessor with special handling for +sdate+ and +edate+. Setting
9
+ # those will also set +sctime+ and +ectime+ respectively.
10
10
  #
11
11
  # @param k [String] Key/property name
12
12
  # @param v [Object] Key/property value
13
13
  def []=(k, v)
14
14
  @self[k] = v
15
- @self['sday'] = ICalPal::RDT.new(*self['sdate'].to_a[0..2]) if k == 'sdate'
15
+
16
+ @self['sctime'] = Time.at(@self['sdate'].to_i, in: 'UTC') if k == 'sdate'
17
+ @self['ectime'] = Time.at(@self['edate'].to_i, in: 'UTC') if k == 'edate'
16
18
  end
17
19
 
18
20
  # Standard accessor with special handling for +age+,
@@ -34,10 +36,10 @@ module ICalPal
34
36
  t += ' at ' unless @self['all_day'].positive?
35
37
  end
36
38
 
37
- unless @self['all_day'] && @self['all_day'].positive? || @self['placeholder']
39
+ unless (@self['all_day'] && @self['all_day'].positive?) || @self['placeholder']
38
40
  t ||= ''
39
- t += "#{@self['sdate'].strftime($opts[:tf])}" if @self['sdate']
40
- t += " - #{@self['edate'].strftime($opts[:tf])}" unless $opts[:eed] || !@self['edate']
41
+ t += "#{@self['sctime'].strftime($opts[:tf])}" if @self['sctime']
42
+ t += " - #{@self['ectime'].strftime($opts[:tf])}" unless $opts[:eed] || !@self['ectime'] || @self['duration'].zero?
41
43
  end
42
44
  t
43
45
 
@@ -45,10 +47,10 @@ module ICalPal
45
47
  (@self['location'])? [ @self['location'], @self['address'] ].join(' ').chop : nil
46
48
 
47
49
  when 'notes' # \n -> :nnr
48
- (@self['notes'])? @self['notes'].strip.gsub(/\n/, $opts[:nnr]) : nil
50
+ (@self['notes'])? @self['notes'].strip.gsub("\n", $opts[:nnr]) : nil
49
51
 
50
52
  when 'sday' # pseudo-property
51
- ICalPal::RDT.new(*@self['sdate'].to_a[0..2])
53
+ RDT.new(*@self['sdate'].to_a[0..2])
52
54
 
53
55
  when 'status' # Integer -> String
54
56
  EventKit::EKEventStatus.select { |_k, v| v == @self['status'] }.keys[0]
@@ -83,23 +85,24 @@ module ICalPal
83
85
 
84
86
  # Convert JSON arrays to Arrays
85
87
  @self['attendees'] = JSON.parse(obj['attendees'])
86
- # rubocop: disable Lint/UselessAssignment
87
88
  @self['xdate'] = JSON.parse(obj['xdate']).map do |k|
88
- k = RDT.new(*Time.at(k + ITIME).to_a.reverse[4..]) if k
89
+ RDT.from_itime(k) if k
89
90
  end
90
- # rubocop: enable Lint/UselessAssignment
91
91
 
92
92
  # Convert iCal dates to normal dates
93
93
  obj.keys.select { |i| i.end_with? '_date' }.each do |k|
94
- t = Time.at(obj[k] + ITIME) if obj[k]
95
- @self["#{k[0]}date"] = RDT.new(*t.to_a.reverse[4..], t.zone) if t
96
- end
94
+ next unless obj[k]
95
+
96
+ begin
97
+ zone = Timezone.fetch(obj['start_tz'])
98
+ rescue Timezone::Error::InvalidZone
99
+ zone = 'UTC'
100
+ end
97
101
 
98
- if @self['start_tz'] == '_float'
99
- tzoffset = Time.zone_offset($now.zone)
102
+ ctime = obj[k] + ITIME
100
103
 
101
- @self['sdate'] = RDT.new(*(@self['sdate'].to_time - tzoffset).to_a.reverse[4..], $now.zone)
102
- @self['edate'] = RDT.new(*(@self['edate'].to_time - tzoffset).to_a.reverse[4..], $now.zone)
104
+ @self["#{k[0]}ctime"] = Time.at(ctime)
105
+ @self["#{k[0]}date"] = RDT.from_time(Time.at(ctime, in: zone))
103
106
  end
104
107
 
105
108
  # Type of calendar event is from
@@ -123,14 +126,17 @@ module ICalPal
123
126
  # Sanity checks
124
127
  return events if nDays > 100_000
125
128
 
129
+ # If multi-day, each (unique) day needs to end at 23:59:59
130
+ self['edate'] = RDT.new(*@self['sdate'].to_a[0..2] + [ 23, 59, 59 ]) if nDays.positive?
131
+
126
132
  # Repeat for multi-day events
127
133
  (nDays + 1).times do |i|
128
134
  break if self['sdate'] > $opts[:to]
129
135
 
130
- $log.debug("multi-day event #{i + 1}") if (i.positive?)
131
-
132
- self['daynum'] = i + 1
133
- events.push(clone) if in_window?(self['sdate'], self['edate'])
136
+ if in_window?(self['sdate'], self['edate'])
137
+ self['daynum'] = i + 1 if nDays.positive?
138
+ events.push(clone)
139
+ end
134
140
 
135
141
  self['sdate'] += 1
136
142
  self['edate'] += 1
@@ -145,78 +151,76 @@ module ICalPal
145
151
  # All occurrences of a recurring event that are within our window
146
152
  def recurring
147
153
  stop = [ $opts[:to], (self['rdate'] || $opts[:to]) ].min
154
+ events = []
155
+ count = 1
148
156
 
149
157
  # See if event ends before we start
150
- if stop < $opts[:from]
151
- $log.debug("#{stop} < #{$opts[:from]}")
152
- return([])
153
- end
158
+ return events if $opts[:from] > stop
154
159
 
155
160
  # Get changes to series
156
- changes = [ { 'orig_date' => -1 } ]
161
+ changes = [ { orig_date: -1 } ]
157
162
  changes += $rows.select { |r| r['orig_item_id'] == self['ROWID'] }
158
163
 
159
- events = []
160
- count = 1
161
-
162
164
  while self['sdate'] <= stop
163
165
  # count
164
166
  break if self['count'].positive? && count > self['count']
165
167
 
166
168
  count += 1
167
169
 
168
- # Handle specifier or clone self
169
- if self['specifier'] && self['specifier'].length.positive?
170
- occurrences = get_occurrences(changes)
171
- else
172
- occurrences = [ clone ]
173
- end
170
+ # Handle specifier
171
+ o = []
172
+ o.push(self) unless self['specifier'] && self['specifier'].length.positive?
173
+ o += occurrences if self['specifier'] && self['specifier'].length.positive?
174
174
 
175
175
  # Check for changes
176
- occurrences.each do |occurrence|
177
- changes.each do |change|
178
- next if change['orig_date'] == self['sdate'].to_i - ITIME
176
+ o.each do |occurrence|
177
+ skip = false
178
+
179
+ changes[1..].each do |change|
180
+ cdate = Time.at(change['start_date'] + ITIME).to_a[3..5].reverse
181
+ odate = occurrence['sdate'].ymd
179
182
 
180
- events.push(occurrence) if in_window?(occurrence['sdate'], occurrence['edate'])
183
+ skip = true if cdate == odate
181
184
  end
182
- end
183
185
 
184
- break if self['specifier']
186
+ events.push(clone(occurrence)) if in_window?(occurrence['sdate'], occurrence['edate']) && !skip
187
+ end
185
188
 
186
- apply_frequency!
189
+ # Handle frequency and interval
190
+ apply_frequency! if self['frequency'] && self['interval']
187
191
  end
188
192
 
189
193
  # Remove exceptions
190
194
  events.delete_if { |event| event['xdate'].any?(event['sdate']) }
191
195
 
192
- events
196
+ events.uniq { |e| e['sdate'] }
193
197
  end
194
198
 
195
199
  private
196
200
 
197
201
  # @!visibility public
198
202
 
199
- # @return a deep clone of self
200
- def clone
201
- Marshal.load(Marshal.dump(self))
203
+ # Deep clone an object
204
+ #
205
+ # @param obj [Object]
206
+ # @return [Object] a deep clone of obj
207
+ def clone(obj = self)
208
+ Marshal.load(Marshal.dump(obj))
202
209
  end
203
210
 
204
- # Get next occurences of a recurring event from a specifier
211
+ # Get next occurrences of a recurring event given a specifier
205
212
  #
206
- # @param _changes [Array] Recurrence changes for the event
207
- # @return [Array<IcalPal::Event>]
208
- def get_occurrences(_changes)
209
- occurrences = []
213
+ # @return [Array<ICalPal::Event>]
214
+ def occurrences
215
+ o = []
210
216
 
211
217
  dow = DOW.keys
212
- dom = [ nil ]
218
+ dom = []
213
219
  moy = 1..12
214
220
  nth = nil
215
221
 
216
- specifier = (self['specifier'])? self['specifier'] : []
217
-
218
222
  # Deconstruct specifier
219
- specifier.split(';').each do |k|
223
+ self['specifier'].split(';').each do |k|
220
224
  j = k.split('=')
221
225
 
222
226
  # D=Day of the week, M=Day of the month, O=Month of the year, S=Nth
@@ -230,38 +234,55 @@ module ICalPal
230
234
  end
231
235
 
232
236
  # Build array of DOWs
233
- dows = [ nil ]
234
- dow.each { |d| dows.push(DOW[d[-2..].to_sym]) }
237
+ dows = []
238
+ dow.each do |d|
239
+ dows.push(DOW[d[-2..].to_sym])
240
+ nth = d[0..-3].to_i if [ '+', '-' ].include? d[0]
241
+ end
235
242
 
236
243
  # Months of the year (O)
237
- moy.each do |m|
238
- next unless m
244
+ moy.each do |mo|
245
+ m = mo.to_i
246
+
247
+ # Set dates to the first of <m>
248
+ nsdate = RDT.new(self['sdate'].year, m, 1, self['sdate'].hour, self['sdate'].minute, self['sdate'].second)
249
+ nedate = RDT.new(self['edate'].year, m, 1, self['edate'].hour, self['edate'].minute, self['edate'].second)
250
+
251
+ # ...but not in the past
252
+ nsdate >>= 12 if nsdate.month < m
253
+ nedate >>= 12 if nedate.month < m
254
+
255
+ next if nsdate > $opts[:to]
256
+ next if ((nedate >> 1) - 1) < $opts[:from]
239
257
 
240
- nsdate = RDT.new(self['sdate'].year, m.to_i, 1)
241
- nedate = RDT.new(self['edate'].year, m.to_i, 1)
258
+ c = clone
242
259
 
243
260
  # Days of the month (M)
244
- dom.each do |x|
245
- next unless x
261
+ dom.each do |day|
262
+ c['sdate'] = RDT.new(nsdate.year, nsdate.month, day.to_i)
263
+ c['edate'] = RDT.new(nedate.year, nedate.month, day.to_i)
246
264
 
247
- self['sdate'] = RDT.new(nsdate.year, nsdate.month, x.to_i)
248
- self['edate'] = RDT.new(nedate.year, nedate.month, x.to_i)
249
- occurrences.push(clone)
265
+ o.push(clone(c)) if in_window?(c['sdate'], c['edate'])
250
266
  end
251
267
 
252
268
  # Days of the week (D)
253
- if nth
254
- self['sdate'] = ICalPal.nth(nth, dows, nsdate)
255
- self['edate'] = ICalPal.nth(nth, dows, nedate)
256
- occurrences.push(clone)
257
- elsif dows[0]
258
- self['sdate'] = RDT.new(nsdate.year, m.to_i, nsdate.wday)
259
- self['edate'] = RDT.new(nedate.year, m.to_i, nedate.wday)
260
- occurrences.push(clone)
269
+ dows.each do |day|
270
+ if nth
271
+ c['sdate'] = ICalPal.nth(nth, day, nsdate)
272
+ c['edate'] = ICalPal.nth(nth, day, nedate)
273
+ else
274
+ diff = day - c['sdate'].wday
275
+ diff += 7 if diff.negative?
276
+
277
+ c['sdate'] += diff
278
+ c['edate'] += diff
279
+ end
280
+
281
+ o.push(clone(c)) if in_window?(c['sdate'], c['edate'])
261
282
  end
262
283
  end
263
284
 
264
- occurrences
285
+ o
265
286
  end
266
287
 
267
288
  # Apply frequency and interval
@@ -278,7 +299,7 @@ module ICalPal
278
299
  when 'yearly' then self[d] >>= self['interval'] * 12
279
300
  else $log.error("Unknown frequency: #{self['frequency']}")
280
301
  end
281
- end if self['frequency'] && self['interval']
302
+ end
282
303
  end
283
304
 
284
305
  # Check if an event starts or ends between from and to, or if it's
data/lib/icalPal.rb CHANGED
@@ -19,8 +19,7 @@ module ICalPal
19
19
  # @return [Class] The subclass of ICalPal
20
20
  def self.call(klass)
21
21
  case klass
22
- when 'accounts' then Store
23
- when 'stores' then Store
22
+ when 'accounts', 'stores' then Store
24
23
  when 'calendars' then Calendar
25
24
  when 'events' then Event
26
25
  when 'tasks' then Reminder
@@ -32,7 +31,7 @@ module ICalPal
32
31
 
33
32
  # Load data
34
33
  def self.load_data(db_file, q)
35
- $log.debug(q.gsub(/\n/, ' '))
34
+ $log.debug(q.gsub("\n", ' '))
36
35
 
37
36
  rows = []
38
37
 
@@ -93,8 +92,7 @@ module ICalPal
93
92
  # @param headers [Array] Key names used as the header row in a CSV::Table
94
93
  # @return [CSV::Row] The +Store+, +Calendar+, or +CalendarItem+ as a CSV::Row
95
94
  def to_csv(headers)
96
- values = []
97
- headers.each { |h| values.push((@self[h].respond_to?(:gsub))? @self[h].gsub(/\n/, '\n') : @self[h]) }
95
+ values = headers.map { |h| (@self[h].respond_to?(:gsub))? @self[h].gsub("\n", '\n') : @self[h] }
98
96
 
99
97
  CSV::Row.new(headers, values)
100
98
  end
@@ -113,13 +111,14 @@ module ICalPal
113
111
  # Get the +n+'th +dow+ in month +m+
114
112
  #
115
113
  # @param n [Integer] Integer between -4 and +4
116
- # @param dow [Array] Days of the week
114
+ # @param dow [Integer] Day of the week
117
115
  # @param m [RDT] The RDT with the year and month we're searching
118
116
  # @return [RDT] The resulting day
119
117
  def self.nth(n, dow, m)
120
- # Get the number of days in the month
121
- a = [ ICalPal::RDT.new(m.year, m.month, 1) ] # First of this month
122
- a[1] = (a[0] >> 1) - 1 # First of next month, minus 1 day
118
+ # Get the number of days in the month by advancing to the first of
119
+ # the next month, then going back one day
120
+ a = [ RDT.new(m.year, m.month, 1, m.hour, m.minute, m.second) ]
121
+ a[1] = (a[0] >> 1) - 1
123
122
 
124
123
  # Reverse it if going backwards
125
124
  a.reverse! if n.negative?
@@ -127,7 +126,7 @@ module ICalPal
127
126
 
128
127
  j = 0
129
128
  a[0].step(a[1], step) do |i|
130
- j += step if dow.any?(i.wday)
129
+ j += step if dow == i.wday
131
130
  return i if j == n
132
131
  end
133
132
  end
@@ -138,7 +137,7 @@ module ICalPal
138
137
  # Days of the week abbreviations used in recurrence rules
139
138
  #
140
139
  # <tt><i>SU, MO, TU, WE, TH, FR, SA</i></tt>
141
- DOW = { 'SU': 0, 'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6 }.freeze
140
+ DOW = { SU: 0, MO: 1, TU: 2, WE: 3, TH: 4, FR: 5, SA: 6 }.freeze
142
141
 
143
142
  # @!group Accessors
144
143
  def [](k)
@@ -156,5 +155,13 @@ module ICalPal
156
155
  def values
157
156
  @self.values
158
157
  end
158
+
159
+ # Like inspect, but easier for humans to read
160
+ #
161
+ # @return [Array<String>] @self as a key=value array, sorted by key
162
+ def dump
163
+ @self.keys.sort.map { |k| "#{k}: #{@self[k]}" }
164
+ end
165
+
159
166
  # @!endgroup
160
167
  end
data/lib/options.rb CHANGED
@@ -27,9 +27,9 @@ module ICalPal
27
27
  @op = OptionParser.new
28
28
  @op.summary_width = 23
29
29
  @op.banner += ' [-c] COMMAND'
30
- @op.version = ICalPal::VERSION
30
+ @op.version = VERSION
31
31
 
32
- @op.accept(ICalPal::RDT) { |s| ICalPal::RDT.conv(s) }
32
+ @op.accept(RDT) { |s| RDT.conv(s) }
33
33
 
34
34
  # head
35
35
  @op.on_head("\nCOMMAND must be one of the following:\n\n")
@@ -83,8 +83,8 @@ module ICalPal
83
83
  # dates
84
84
  @op.separator("\nChoosing dates:\n\n")
85
85
 
86
- @op.on('--from=DATE', ICalPal::RDT, 'List events starting on or after DATE')
87
- @op.on('--to=DATE', ICalPal::RDT, 'List events starting on or before DATE',
86
+ @op.on('--from=DATE', RDT, 'List events starting on or after DATE')
87
+ @op.on('--to=DATE', RDT, 'List events starting on or before DATE',
88
88
  'DATE can be yesterday, today, tomorrow, +N, -N, or anything accepted by DateTime.parse()',
89
89
  'See https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html#method-c-parse')
90
90
  @op.separator('')
@@ -172,6 +172,14 @@ module ICalPal
172
172
  @op.on_tail('%s%s %sAdditional arguments' % pad('ICALPAL'))
173
173
  @op.on_tail('%s%s %sAdditional arguments from a file' % pad('ICALPAL_CONFIG'))
174
174
  @op.on_tail("%s%s %s(default: #{$defaults[:common][:cf]})" % pad(''))
175
+
176
+ @op.on_tail('')
177
+
178
+ note = 'Do not quote or escape values.'
179
+ note += ' Options set in ICALPAL override ICALPAL_CONFIG.'
180
+ note += ' Options on the command line override ICALPAL.'
181
+
182
+ @op.on_tail("#{@op.summary_indent}#{note}")
175
183
  end
176
184
 
177
185
  # Parse options from the CLI and merge them with other sources
@@ -184,11 +192,45 @@ module ICalPal
184
192
  env = {}
185
193
  cf = {}
186
194
 
187
- # Load from CLI, environment, configuration file
195
+ # Load from CLI
188
196
  @op.parse!(into: cli)
189
- @op.parse!(ENV['ICALPAL'].split, into: env) rescue nil
190
- cli[:cf] ||= ENV['ICALPAL_CONFIG'] || $defaults[:common][:cf]
191
- @op.parse!(File.read(File.expand_path(cli[:cf])).split, into: cf) rescue nil
197
+
198
+ # Environment variable needs special parsing.
199
+ # OptionParser.parse doesn't handle whitespace in a
200
+ # comma-separated value.
201
+ begin
202
+ o = []
203
+
204
+ ENV['ICALPAL'].gsub(/^-/, ' -').split(' -').each do |e|
205
+ a = e.split(' ', 2)
206
+
207
+ if a[0]
208
+ o.push("-#{a[0]}")
209
+ o.push(a[1]) if a[1]
210
+ end
211
+ end
212
+
213
+ @op.parse!(o, into: env)
214
+ end if ENV['ICALPAL']
215
+
216
+ # Configuration file needs special parsing for the same reason
217
+ begin
218
+ o = []
219
+
220
+ cli[:cf] ||= ENV['ICALPAL_CONFIG'] || $defaults[:common][:cf]
221
+
222
+ File.read(File.expand_path(cli[:cf])).split("\n").each do |line|
223
+ a = line.split(' ', 2)
224
+
225
+ if a[0] && a[0][0] != '#'
226
+ o.push(a[0])
227
+ o.push(a[1]) if a[1]
228
+ end
229
+ end
230
+
231
+ @op.parse!(o, into: cf)
232
+ rescue StandardError
233
+ end
192
234
 
193
235
  cli[:cmd] ||= @op.default_argv[0]
194
236
  cli[:cmd] ||= env[:cmd] if env[:cmd]
@@ -244,6 +286,7 @@ module ICalPal
244
286
  opts[:to] += 1 if opts[:to]
245
287
  opts[:to] ||= opts[:from] + 1 if opts[:from]
246
288
  opts[:to] = opts[:from] + opts[:days] if opts[:days]
289
+ opts[:to] = RDT.new(*opts[:to].to_a[0..2] + [ 23, 59, 59 ])
247
290
  opts[:days] ||= Integer(opts[:to] - opts[:from])
248
291
  opts[:from] = $now if opts[:n]
249
292
  end
data/lib/rdt.rb CHANGED
@@ -1,9 +1,22 @@
1
- require 'date'
2
-
3
1
  module ICalPal
4
2
  # Child class of DateTime that adds support for relative dates (<em><b>R</b>elative<b>D</b>ate<b>T</b>ime</em>).
5
3
  class RDT < DateTime
6
4
 
5
+ # Create a new RDT from a Time object
6
+ def self.from_time(t)
7
+ new(*t.to_a[0..5].reverse)
8
+ end
9
+
10
+ # Create a new RDT from seconds since epoch
11
+ def self.from_epoch(s)
12
+ from_time(Time.at(s))
13
+ end
14
+
15
+ # Create a new RDT from seconds since iCal epoch
16
+ def self.from_itime(s)
17
+ from_epoch(s + ITIME)
18
+ end
19
+
7
20
  # Convert a String to an RDT
8
21
  #
9
22
  # @param str [String] can be +yesterday+, +today+, +tomorrow+,
@@ -28,7 +41,7 @@ module ICalPal
28
41
  # @return [String] A string representation of self relative to
29
42
  # today.
30
43
  def to_s
31
- return strftime($opts[:df]) if $opts[:nrd] && $opts[:df]
44
+ return strftime($opts[:df]) if $opts && $opts[:nrd] && $opts[:df]
32
45
 
33
46
  case Integer(RDT.new(year, month, day) - $today)
34
47
  when -2 then 'day before yesterday'
@@ -36,7 +49,7 @@ module ICalPal
36
49
  when 0 then 'today'
37
50
  when 1 then 'tomorrow'
38
51
  when 2 then 'day after tomorrow'
39
- else strftime($opts[:df]) if $opts[:df]
52
+ else strftime($opts[:df]) if $opts && $opts[:df]
40
53
  end
41
54
  end
42
55
 
@@ -54,6 +67,11 @@ module ICalPal
54
67
  to_time.to_i
55
68
  end
56
69
 
70
+ # @return [Array] Only the year, month and day of self
71
+ def ymd
72
+ [ year, month, day ]
73
+ end
74
+
57
75
  # @see ICalPal::RDT.to_s
58
76
  #
59
77
  # @return [Boolean]
data/lib/utils.rb CHANGED
@@ -9,10 +9,6 @@ def xmlify(key, value)
9
9
  # Nil
10
10
  when NilClass then "<#{key}/>"
11
11
 
12
- # String, Integer
13
- when String then "<#{key}>#{value}</#{key}>"
14
- when Integer then "<#{key}>#{value}</#{key}>"
15
-
16
12
  # Array
17
13
  when Array
18
14
  # Treat empty arrays as nil values
@@ -22,9 +18,6 @@ def xmlify(key, value)
22
18
  value.each { |x| retval += xmlify("#{key}0", x) }
23
19
  "<#{key}>#{retval}</#{key}>"
24
20
 
25
- # RDT
26
- when ICalPal::RDT then "<#{key}>#{value}</#{key}>"
27
-
28
21
  # Unknown
29
22
  else "<#{key}>#{value}</#{key}>"
30
23
  end
data/lib/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module ICalPal
2
2
  NAME = 'icalPal'.freeze
3
- VERSION = '3.3.0'.freeze
3
+ VERSION = '3.4.0'.freeze
4
4
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icalPal
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Rosen
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-24 00:00:00.000000000 Z
10
+ date: 2025-04-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: nokogiri-plist
@@ -37,6 +37,26 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: 2.6.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: timezone
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.99'
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: 1.3.0
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0.99'
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: 1.3.0
40
60
  description: |
41
61
  Inspired by icalBuddy and maintains close compatability. Includes
42
62
  many additional features for querying, filtering, and formatting.
@@ -68,6 +88,7 @@ licenses:
68
88
  - GPL-3.0-or-later
69
89
  metadata:
70
90
  bug_tracker_uri: https://github.com/ajrosen/icalPal/issues
91
+ rubygems_mfa_required: 'true'
71
92
  post_install_message: |2+
72
93
 
73
94
  Note: icalPal requires "Full Disk Access" in System Settings to access your calendar.