icalPal 3.9.3 → 3.10.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: 4853d1f050b001c63c1890618f5393eaee86c812f86cacf8be8b9da483fc67d1
4
- data.tar.gz: ae170e19546fc153bdfbcd6cd89e02ac91c694e2c393e6624e31d437a5b03a34
3
+ metadata.gz: 014f8b5179cd56cb757404df75a3d23145907d2f40b4186031ed5028ad5e92af
4
+ data.tar.gz: 8abe6a5c80c695fa61b0000ae20c52fcbd1fab29ab326fcb89fe1a4d469fae82
5
5
  SHA512:
6
- metadata.gz: 3bf5728beb2504a19f2fae851413c26c6ee192fe2f09e066f8f9b1887e3eb16102eb9a89a4eb5e717e727564e8ca4a74bd09dc3d2743974cf2d33a24d7b25b5d
7
- data.tar.gz: b60b90e37a562ca7df73c53e0e6ade623baa857b3f520a7faded502e7d588c7cbcb3ef2ee02bc9f486bd840f11b4e7638d92f769570e6b216e3bff9b2eaae5af
6
+ metadata.gz: 24bafd17756e9e4ccce28dc0b23ea421d334f3d9c58f3fe62dd07cf26777b47c56587a52e61c5995a730b823a3ad2f0ebc8bef460205ebcde981df3e0bd35108
7
+ data.tar.gz: a21caa7650b168542625a176909e05e0f87825c452015e7292f5297aa8b9d96863dc22c1f921a197a97769c9b3fb5db0a593adf1b22784b1874e03ecb05a8b34
data/README.md CHANGED
@@ -1,15 +1,18 @@
1
- [![Gem Version](https://badge.fury.io/rb/icalPal.svg)](https://badge.fury.io/rb/icalPal)
1
+ ![GitHub Release](https://img.shields.io/github/v/release/ajrosen/icalPal?display_name=tag&logo=rubygems&label=gem%20version)
2
+ [![Downloads](https://img.shields.io/gem/dtv/icalPal?label=downloads)](https://badge.fury.io/rb/icalPal.svg?icon=si%3Arubygems&icon_color=%23ff0000)
3
+ [![Downloads](https://img.shields.io/gem/dt/icalPal?label=total%20downloads)](https://badge.fury.io/rb/icalPal.svg?icon=si%3Arubygems&icon_color=%23ff0000)
2
4
 
3
5
  # icalPal
4
6
 
5
7
  ## Description
6
8
 
7
- icalPal is a command-line tool to query macOS Calendar and Reminders
8
- databases for accounts, calendars, events, and tasks. It can be run
9
- on any system with [Ruby](https://www.ruby-lang.org/) and access to a
10
- Calendar or Reminders database.
9
+ icalPal is a command-line tool to query macOS *Calendar* and
10
+ *Reminders* databases for accounts, calendars, events, and tasks. It
11
+ can be run on any system with [Ruby](https://www.ruby-lang.org/) and
12
+ access to a Calendar or Reminders database.
11
13
 
12
14
  <!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
15
+
13
16
  **Table of Contents**
14
17
 
15
18
  - [Installation](#installation)
@@ -26,13 +29,13 @@ Calendar or Reminders database.
26
29
 
27
30
  ## Installation
28
31
 
29
- As a system-wide Ruby gem:
32
+ Install icalPal for all users:
30
33
 
31
34
  ```
32
35
  gem install icalPal
33
36
  ```
34
37
 
35
- In your home directory:
38
+ For only you:
36
39
 
37
40
  ```
38
41
  gem install --user-install icalPal
@@ -64,6 +67,8 @@ differences to be aware of.
64
67
 
65
68
  ### Additional commands
66
69
 
70
+ icalPal also supports additional commands.
71
+
67
72
  ```icalPal accounts```
68
73
 
69
74
  Shows a list of enabled Calendar accounts. Internally they are known
@@ -79,7 +84,7 @@ Shows only reminders that have a due date.
79
84
  *remindersDueBefore* can be used instead of *tasks*
80
85
 
81
86
  Reminders can also be viewed in the *Scheduled Reminders* calendar,
82
- using the *tasks* commands. Repeating reminders are treated the same
87
+ using the *events* commands. Repeating reminders are treated the same
83
88
  as repeating events.
84
89
 
85
90
  ### Additional options
@@ -98,39 +103,35 @@ as repeating events.
98
103
  * ```--color``` uses a wider color palette. Colors are what you have chosen in the Calendar and Reminders apps, including custom colors
99
104
  * ```--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
100
105
 
101
- Because icalPal is written in Ruby, and not a native Mac application,
102
- you can run it just about anywhere. It's been tested with the
103
- versions of Ruby included with macOS Sequoia and Tahoe (2.6.10) and
104
- [Homebrew](https://brew.sh/) (3.4.x).
105
-
106
106
  ### Additional properties
107
107
 
108
108
  Several additional properties are available for each command.
109
109
 
110
110
  * Accounts
111
111
  * account
112
+ * delegations
112
113
  * notes
113
114
  * owner
114
115
  * type
115
- * delegations
116
116
 
117
117
  * Calendar
118
118
  * account
119
- * shared\_owner_name, shared\_owner_address
119
+ * locale
120
+ * notes
121
+ * published_URL
120
122
  * self\_identity_email, owner\_identity_email
123
+ * shared\_owner_name, shared\_owner_address
124
+ * sharees
121
125
  * subcal_account_id, subcal_url
122
- * published_URL
123
- * notes
124
- * locale
125
126
 
126
127
  * Tasks
127
- * id
128
- * grocery
128
+ * assignee
129
129
  * completed
130
+ * grocery
130
131
  * group
132
+ * id
131
133
  * section
132
134
  * tags
133
- * assignee
134
135
  * timezone
135
136
  * Notifications
136
137
  * due (due_date formatted with --df and --tf options)
@@ -143,7 +144,7 @@ Several additional properties are available for each command.
143
144
  icalPal: Usage: icalPal [options] [-c] COMMAND
144
145
 
145
146
  COMMAND must be one of the following:
146
- ```
147
+
147
148
  events Print events
148
149
  tasks Print tasks
149
150
  calendars Print calendars
@@ -159,29 +160,27 @@ COMMAND must be one of the following:
159
160
 
160
161
  stores can be used instead of accounts
161
162
  reminders can be used instead of tasks
162
- ```
163
163
 
164
164
  Global options:
165
- ```
165
+
166
166
  -c, --cmd=COMMAND Command to run
167
167
  --db=DB Use DB file instead of Calendar
168
- (default: ["$HOME/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb", $HOME/Library/Calendars/Calendar.sqlitedb]
168
+ (default: ["$HOME/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb", "$HOME/Library/Calendars/Calendar.sqlitedb"]
169
169
  For the tasks commands this should be a directory containing .sqlite files
170
- (default: "$HOME/Library/Group Containers/group.com.apple.reminders/Container_v1/Stores")
170
+ (default: ["$HOME/Library/Group Containers/group.com.apple.reminders/Container_v1/Stores"])
171
171
  --cf=FILE Set config file path (default: $HOME/.icalpal)
172
172
  --norc Ignore ICALPAL and ICALPAL_CONFIG environment variables
173
173
  -o, --output=FORMAT Print as FORMAT (default: default)
174
174
  [ansi, csv, default, hash, html, json, md, rdoc, remind, toc, xml, yaml]
175
- ```
176
175
 
177
- Including/excluding accounts, calendars, reminders and items:
178
- ```
176
+ Including/excluding accounts, calendars, items:
177
+
179
178
  --is=ACCOUNTS List of accounts to include
180
179
  --es=ACCOUNTS List of accounts to exclude
181
180
 
182
181
  --it=TYPES List of calendar types to include
183
182
  --et=TYPES List of calendar types to exclude
184
- [Local, Exchange, CalDAV, MobileMe, Subscribed, Birthdays]
183
+ [Local, Exchange, CalDAV, MobileMe, Subscribed, Birthdays, Reminders]
185
184
 
186
185
  --ic=CALENDARS List of calendars to include
187
186
  --ec=CALENDARS List of calendars to exclude
@@ -192,11 +191,10 @@ Including/excluding accounts, calendars, reminders and items:
192
191
  --id Include completed reminders
193
192
  --ed Exclude uncompleted reminders
194
193
 
195
- --match=FIELD=REGEX Include only items whose FIELD matches REGEXP (ignoring case)
196
- ```
194
+ --match=FIELD=REGEX Include only items whose FIELD matches REGEX (ignoring case)
197
195
 
198
196
  Choosing dates:
199
- ```
197
+
200
198
  --from=DATE List events starting on or after DATE
201
199
  --to=DATE List events starting on or before DATE
202
200
  DATE can be yesterday, today, tomorrow, +N, -N, or anything accepted by DateTime.parse()
@@ -207,10 +205,9 @@ Choosing dates:
207
205
  --sed Show empty dates with --sd
208
206
  --ia Include only all-day events
209
207
  --ea Exclude all-day events
210
- ```
211
208
 
212
209
  Choose properties to include in the output:
213
- ```
210
+
214
211
  --iep=PROPERTIES List of properties to include
215
212
  --eep=PROPERTIES List of properties to exclude
216
213
  --aep=PROPERTIES List of properties to include in addition to the default list
@@ -231,10 +228,9 @@ Choose properties to include in the output:
231
228
 
232
229
  Use 'all' for PROPERTIES to include all available properties (except any listed in --eep)
233
230
  Use 'list' for PROPERTIES to list all available properties and exit
234
- ```
235
231
 
236
232
  Formatting the output:
237
- ```
233
+
238
234
  --li=N Show at most N items (default: 0 for no limit)
239
235
 
240
236
  --sc Separate by calendar
@@ -261,24 +257,21 @@ Formatting the output:
261
257
 
262
258
  -f Format output using standard ANSI colors
263
259
  --color Format output using a larger color palette
264
- ```
265
260
 
266
261
  Help:
267
- ```
262
+
268
263
  -h, --help Show this message
269
- -V, -v, --version Show version and exit (3.9.1)
264
+ -V, -v, --version Show version and exit (3.10.0)
270
265
  -d, --debug=LEVEL Set the logging level (default: warn)
271
266
  [debug, info, warn, error, fatal]
272
- ```
273
267
 
274
268
  Environment variables:
275
- ```
269
+
276
270
  ICALPAL Additional arguments
277
271
  ICALPAL_CONFIG Additional arguments from a file
278
272
  (default: $HOME/.icalpal)
279
273
 
280
274
  Do not quote or escape values. Options set in ICALPAL override ICALPAL_CONFIG. Options on the command line override ICALPAL.
281
- ```
282
275
 
283
276
  ## Output formats
284
277
 
@@ -295,35 +288,9 @@ properly formatted.
295
288
 
296
289
  Other formats such as ANSI, HTML, Markdown, RDoc, and TOC, use Ruby's
297
290
  [RDoc::Markup](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup.html)
298
- framework to build and render the items.
299
-
300
- Each item to be printed is a new
301
- [RDoc::Markup::Document](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Document.html).
302
-
303
- When using one of the _separate by_ options, a section header is added
304
- first. The section contains:
305
-
306
- * [RDoc::Markup::BlankLine](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/BlankLine.html)
307
- (unless this is the first section)
308
- * RDoc::Markup::Heading (level 1)
309
- * [RDoc::Markup::Rule](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Rule.html)
310
-
311
- The rest of the document is a series of
312
- [RDoc::Markup::List](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/List.html)
313
- objects, one for each of the item's properties:
314
-
315
- * [RDoc::Markup::List](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/List.html)
316
- * RDoc::Markup::Heading (level 2)
317
- * [RDoc::Markup::BlankLine](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/BlankLine.html)
318
- * [RDoc::Markup::ListItem](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/ListItem.html)
319
- * [RDoc::Markup::Paragraph](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Paragraph.html)
320
-
321
- The document will also include a number of
322
- [RDoc::Markup::Verbatim](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Verbatim.html)
323
- and
324
- [RDoc::Markup::Raw](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Raw.html)
325
- items. They are not included in the output, but are used to pass
326
- information about the item and property to the default formatter.
291
+ framework to build and render the items. See
292
+ [RDoc](https://github.com/ajrosen/icalPal/blob/main/RDoc.md) for a
293
+ breakdown of how icalPal uses RDoc::Markup.
327
294
 
328
295
  ## History
329
296
 
@@ -341,9 +308,14 @@ Lawton](https://github.com/jimlawton) that it even compiles anymore.
341
308
  Instead of trying to understand and extend the existing code, I chose
342
309
  to start anew using my language of choice:
343
310
  [Ruby](https://www.ruby-lang.org). Using Ruby meant there is *much*
344
- less code; a little over 2,000 lines vs. 7,000. It also means icalPal
311
+ less code; a little over 2,200 lines vs. 7,000. It also means icalPal
345
312
  is multi-platform.
346
313
 
314
+ Because icalPal is written in Ruby, and not a native Mac application,
315
+ you can run it just about anywhere. It's been tested with the
316
+ versions of Ruby included with macOS Sequoia and Tahoe (2.6.10) and
317
+ [Homebrew](https://brew.sh/) (3.4.x).
318
+
347
319
  I won't pretend to understand **why** you would want to run this on
348
320
  Linux or Windows. But since icalPal is written in Ruby and gets its
349
321
  data directly from the Calendar and Reminders database files instead
data/bin/icalPal CHANGED
@@ -231,14 +231,14 @@ end
231
231
 
232
232
  # Sort the rows
233
233
  begin
234
- sort = []
235
- sort.push $opts[:sep] if $opts[:sep]
236
- sort.push $opts[:sort] if $opts[:sort]
237
- sort.push 'sdate'
234
+ $sort_attrs = []
235
+ $sort_attrs.push $opts[:sep] if $opts[:sep]
236
+ $sort_attrs.push $opts[:sort] if $opts[:sort]
237
+ $sort_attrs.push 'sdate'
238
238
 
239
- $log.info("Sorting #{$items.count} items by #{sort}, reverse #{$opts[:reverse].inspect}")
239
+ $log.info("Sorting #{$items.count} items by #{$sort_attrs}, reverse #{$opts[:reverse].inspect}")
240
240
 
241
- $items.sort_by! { |i| [ i[sort[0]], i[sort[1]], i[sort[2]] ] }
241
+ $items.sort!
242
242
  $items.reverse! if $opts[:reverse]
243
243
  rescue Exception => e
244
244
  $log.info("Sorting failed: #{e}\n")
@@ -321,8 +321,10 @@ items.each_with_index do |i, j|
321
321
  doc << RDoc::Markup::Raw.new($opts[:sep])
322
322
 
323
323
  doc << RDoc::Markup::BlankLine.new if j.positive?
324
- doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
325
- doc << RDoc::Markup::Rule.new(0)
324
+ if i[$opts[:sep]]
325
+ doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
326
+ doc << RDoc::Markup::Rule.new(0)
327
+ end
326
328
 
327
329
  section = i[$opts[:sep]]
328
330
  end
data/bin/icalpal CHANGED
@@ -231,14 +231,14 @@ end
231
231
 
232
232
  # Sort the rows
233
233
  begin
234
- sort = []
235
- sort.push $opts[:sep] if $opts[:sep]
236
- sort.push $opts[:sort] if $opts[:sort]
237
- sort.push 'sdate'
234
+ $sort_attrs = []
235
+ $sort_attrs.push $opts[:sep] if $opts[:sep]
236
+ $sort_attrs.push $opts[:sort] if $opts[:sort]
237
+ $sort_attrs.push 'sdate'
238
238
 
239
- $log.info("Sorting #{$items.count} items by #{sort}, reverse #{$opts[:reverse].inspect}")
239
+ $log.info("Sorting #{$items.count} items by #{$sort_attrs}, reverse #{$opts[:reverse].inspect}")
240
240
 
241
- $items.sort_by! { |i| [ i[sort[0]], i[sort[1]], i[sort[2]] ] }
241
+ $items.sort!
242
242
  $items.reverse! if $opts[:reverse]
243
243
  rescue Exception => e
244
244
  $log.info("Sorting failed: #{e}\n")
@@ -321,8 +321,10 @@ items.each_with_index do |i, j|
321
321
  doc << RDoc::Markup::Raw.new($opts[:sep])
322
322
 
323
323
  doc << RDoc::Markup::BlankLine.new if j.positive?
324
- doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
325
- doc << RDoc::Markup::Rule.new(0)
324
+ if i[$opts[:sep]]
325
+ doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
326
+ doc << RDoc::Markup::Rule.new(0)
327
+ end
326
328
 
327
329
  section = i[$opts[:sep]]
328
330
  end
data/lib/calendar.rb CHANGED
@@ -12,6 +12,12 @@ module ICalPal
12
12
  end
13
13
  end
14
14
 
15
+ def initialize(obj)
16
+ super
17
+
18
+ @self['sharees'] = JSON.parse(obj['sharees'])
19
+ end
20
+
15
21
  QUERY = <<~SQL.freeze
16
22
  SELECT DISTINCT
17
23
 
@@ -23,6 +29,8 @@ c1.title AS calendar,
23
29
  c1.shared_owner_name,
24
30
  c1.shared_owner_address,
25
31
 
32
+ json_group_array(i1.display_name) AS sharees,
33
+
26
34
  c1.published_URL,
27
35
  c1.self_identity_email,
28
36
  c1.owner_identity_email,
@@ -34,10 +42,14 @@ c1.locale
34
42
  FROM #{self.name.split('::').last} c1
35
43
 
36
44
  JOIN Store s1 ON c1.store_id = s1.rowid
45
+ LEFT OUTER JOIN Sharee s2 ON c1.rowid = s2.owner_id
46
+ LEFT OUTER JOIN Identity i1 ON s2.identity_id = i1.rowid
37
47
 
38
48
  WHERE s1.disabled IS NOT 1
39
49
  AND s1.display_order IS NOT -1
40
50
  AND c1.flags IS NOT 519
51
+
52
+ GROUP BY c1.title
41
53
  SQL
42
54
 
43
55
  end
data/lib/defaults.rb CHANGED
@@ -49,31 +49,6 @@ $defaults = {
49
49
  sort: 'prio',
50
50
  },
51
51
 
52
- undatedTasks: {
53
- dated: 1,
54
- db: [ ICalPal::Reminder::DB_PATH ],
55
- iep: %w[ title notes due priority ],
56
- ps: [ "\n " ],
57
- sort: 'prio',
58
- },
59
-
60
- datedTasks: {
61
- dated: 2,
62
- db: [ ICalPal::Reminder::DB_PATH ],
63
- iep: %w[ title notes due priority ],
64
- ps: [ "\n " ],
65
- sort: 'prio',
66
- },
67
-
68
- tasksDueBefore: {
69
- dated: 3,
70
- db: [ ICalPal::Reminder::DB_PATH ],
71
- iep: %w[ title notes due priority ],
72
- ps: [ "\n " ],
73
- sort: 'prio',
74
- to: $today,
75
- },
76
-
77
52
  accounts: {
78
53
  iep: %w[ account type ],
79
54
  sort: 'account',
data/lib/event.rb CHANGED
@@ -16,14 +16,14 @@ module ICalPal
16
16
  end
17
17
 
18
18
  # Standard accessor with special handling for +age+,
19
- # +availability+, +datetime+, +location+, +notes+, +status+,
20
- # +title+, and +uid+
19
+ # +availability+, +datetime+, +location+, +notes+, +sday+,
20
+ # +status+, +uid+, and +event+/+name+/+title+
21
21
  #
22
22
  # @param k [String] Key/property name
23
23
  def [](k)
24
24
  case k
25
25
  when 'age' # pseudo-property
26
- @self['sdate'].year - @self['edate'].year
26
+ @self['sdate'].year - Time.at(@self['start_date'] + ITIME).year
27
27
 
28
28
  when 'availability' # Integer -> String
29
29
  EventKit::EKEventAvailability.select { |_k, v| v == @self['availability'] }.keys
@@ -48,7 +48,7 @@ module ICalPal
48
48
  (@self['notes'])? @self['notes'].strip.gsub("\n", $opts[:nnr]) : nil
49
49
 
50
50
  when 'sday' # pseudo-property
51
- @self['sdate'].day_start
51
+ @self['sdate'].day_start(0)
52
52
 
53
53
  when 'status' # Integer -> String
54
54
  EventKit::EKEventStatus.select { |_k, v| v == @self['status'] }.keys[0]
@@ -76,7 +76,7 @@ module ICalPal
76
76
  'sdate' => obj,
77
77
  'placeholder' => true,
78
78
  'title' => 'Nothing.',
79
- } if obj.is_a?(DateTime) && $opts[:sed]
79
+ } if $opts[:sed] && obj.is_a?(DateTime)
80
80
 
81
81
  super
82
82
 
@@ -90,10 +90,13 @@ module ICalPal
90
90
  obj.keys.select { |i| i.end_with? '_date' }.each do |k|
91
91
  next unless obj[k]
92
92
 
93
+ zone = nil
94
+ zone = '+00:00' if obj['all_day'].positive?
95
+
93
96
  # Save as seconds, Time, RDT
94
97
  ctime = obj[k] + ITIME
95
- ctime -= $now.utc_offset if obj['start_tz'] == '_float'
96
- ttime = Time.at(ctime)
98
+ ctime -= Time.at(ctime).utc_offset if obj["#{k}_tz"] == '_float' && !zone
99
+ ttime = Time.at(ctime, in: zone)
97
100
 
98
101
  @self["#{k[0]}seconds"] = ctime
99
102
  @self["#{k[0]}ctime"] = ttime
@@ -173,7 +176,7 @@ module ICalPal
173
176
  skip = true if codate == odate
174
177
  end
175
178
 
176
- events.push(clone(occurrence)) if in_window?(occurrence['sdate'], occurrence['edate']) && !skip
179
+ events.push(clone(occurrence)) if !skip && in_window?(occurrence['sdate'], occurrence['edate'])
177
180
  end
178
181
 
179
182
  # Handle frequency and interval
@@ -261,11 +264,16 @@ module ICalPal
261
264
  c['sdate'] = ICalPal.nth(nth, day, nsdate)
262
265
  c['edate'] = ICalPal.nth(nth, day, nedate)
263
266
  else
264
- diff = day - c['sdate'].wday
265
- diff += 7 if diff.negative?
267
+ %w[ sdate edate ].each do |d|
268
+ diff = day - c['sdate'].wday
269
+ diff += 7 if diff.negative?
270
+
271
+ t1 = Time.at(c[d].to_time)
272
+ t2 = Time.at(t1.to_i + (diff * 86_400))
273
+ t2 += (t1.gmt_offset - t2.gmt_offset)
266
274
 
267
- c['sdate'] += diff
268
- c['edate'] += diff
275
+ c[d] = RDT.from_time(t2)
276
+ end
269
277
  end
270
278
 
271
279
  o.push(clone(c)) if in_window?(c['sdate'], c['edate'])
@@ -277,18 +285,18 @@ module ICalPal
277
285
 
278
286
  # Apply frequency and interval
279
287
  def apply_frequency!
280
- # Leave edate alone for birthdays to compute age
281
- dates = [ 'sdate' ]
282
- dates << 'edate' unless self['calendar'].include?('Birthday')
283
-
284
- dates.each do |d|
288
+ %w[ sdate edate ].each do |d|
285
289
  case EventKit::EKRecurrenceFrequency[self['frequency'] - 1]
286
- when 'daily' then self[d] += self['interval']
287
- when 'weekly' then self[d] += self['interval'] * 7
288
- when 'monthly' then self[d] >>= self['interval']
289
- when 'yearly' then self[d] >>= self['interval'] * 12
290
+ when 'daily' then nd = self[d] + self['interval']
291
+ when 'weekly' then nd = self[d] + (self['interval'] * 7)
292
+ when 'monthly' then nd = self[d] >> self['interval']
293
+ when 'yearly' then nd = self[d] >> (self['interval'] * 12)
290
294
  else $log.error("Unknown frequency: #{self['frequency']}")
291
295
  end
296
+
297
+ # Create a new Time object in case we crossed a daylight saving change
298
+ t = Time.parse("#{nd.year}-#{nd.month}-#{nd.day} #{nd.hour}:#{nd.min}:#{nd.sec}")
299
+ self[d] = RDT.from_time(t)
292
300
  end
293
301
  end
294
302
 
@@ -307,8 +315,8 @@ module ICalPal
307
315
  $log.debug("not now: #{s} to #{e} vs. #{$now}")
308
316
  false
309
317
  end
310
- elsif ([ s, e ].max >= $opts[:from] && s < $opts[:to])
311
- $log.debug("in window: #{s} to #{e} vs. #{$opts[:from]} to #{$opts[:to]}")
318
+ elsif (s < $opts[:to] && [ s, e ].max >= $opts[:from])
319
+ $log.debug("#{@self['title']} in window: #{s} to #{e} vs. #{$opts[:from]} to #{$opts[:to]}")
312
320
  true
313
321
  else
314
322
  $log.debug("not in window: #{s} to #{e} vs. #{$opts[:from]} to #{$opts[:to]}")
@@ -335,6 +343,7 @@ CalendarItem.all_day,
335
343
  CalendarItem.availability,
336
344
  CalendarItem.conference_url_detected,
337
345
  CalendarItem.description AS notes,
346
+ CalendarItem.end_tz,
338
347
  CalendarItem.has_recurrences,
339
348
  CalendarItem.invitation_status,
340
349
  CalendarItem.orig_item_id,
data/lib/icalPal.rb CHANGED
@@ -149,6 +149,25 @@ module ICalPal
149
149
  @self.values
150
150
  end
151
151
 
152
+ # @see Array.<=>
153
+ #
154
+ # If either self or other is nil, but not both, the nil object is
155
+ # always less than
156
+ def <=>(other)
157
+ $sort_attrs.each do |s|
158
+ next if self[s] == other[s]
159
+
160
+ # nil is always less than
161
+ return -1 if other[s].nil?
162
+ return 1 if self[s].nil?
163
+
164
+ return -1 if self[s] < other[s]
165
+ return 1 if self[s] > other[s]
166
+ end
167
+
168
+ 0
169
+ end
170
+
152
171
  # Like inspect, but easier for humans to read
153
172
  #
154
173
  # @return [Array<String>] @self as a key=value array, sorted by key
data/lib/options.rb CHANGED
@@ -268,17 +268,29 @@ module ICalPal
268
268
 
269
269
  when 'Remaining'
270
270
  cli[:from] = RDT.from_time($now)
271
- cli[:to] = $today.day_end
271
+ cli[:to] = $today.day_end(0)
272
272
  cli[:days] = 1
273
273
  end
274
274
  end
275
275
 
276
276
  # Handle tasks command variants
277
- if cli[:cmd] == 'tasksDueBefore'
278
- cli.delete(:days) unless cli[:days]
279
- cli[:from] = RDT.from_epoch(0) unless cli[:from]
277
+ if cli[:cmd] =~ /tasks/i
278
+ case cli[:cmd]
279
+ when 'undatedTasks'
280
+ cli[:dated] = 1
281
+
282
+ when 'datedTasks'
283
+ cli[:dated] = 2
284
+
285
+ when 'tasksDueBefore'
286
+ cli[:dated] = 3
287
+ cli.delete(:days) unless cli[:days]
288
+ cli[:from] = RDT.from_epoch(0) unless cli[:from]
289
+ cli[:to] = $today unless cli[:to]
290
+ end
291
+
292
+ cli[:cmd] = 'tasks'
280
293
  end
281
- cli[:cmd] = 'tasks' if %w[ datedTasks undatedTasks tasksDueBefore ].include? cli[:cmd]
282
294
 
283
295
  # Must have a valid command
284
296
  raise(OptionParser::InvalidArgument, "Unknown COMMAND #{cli[:cmd]}") unless (COMMANDS.any? cli[:cmd])
data/lib/rdt.rb CHANGED
@@ -68,14 +68,26 @@ module ICalPal
68
68
  to_time.to_i
69
69
  end
70
70
 
71
+ # @param [Integer] Optional UTC offset
71
72
  # @return [RDT] Self at 00:00:00
72
- def day_start
73
- RDT.new(year, month, day, 0, 0, 0, zone)
73
+ def day_start(z = zone)
74
+ RDT.new(year, month, day, 0, 0, 0, z)
74
75
  end
75
76
 
77
+ # @param [Integer] Optional UTC offset
76
78
  # @return [RDT] Self at 23:59:59
77
- def day_end
78
- RDT.new(year, month, day, 23, 59, 59, zone)
79
+ def day_end(z = zone)
80
+ RDT.new(year, month, day, 23, 59, 59, z)
81
+ end
82
+
83
+ # @return [Array] Only the year, month and day of self
84
+ def ymd
85
+ [ year, month, day ]
86
+ end
87
+
88
+ # @return [Array] Only the hour, min and sec of self
89
+ def hms
90
+ [ hour, min, sec ]
79
91
  end
80
92
 
81
93
  # @return [Array] Only the year, month and day of self
data/lib/reminder.rb CHANGED
@@ -56,6 +56,12 @@ module ICalPal
56
56
  when 'sdate' # For sorting
57
57
  @self['due_date']
58
58
 
59
+ when 'sday'
60
+ @self['due'].day_start(0) if @self['due']
61
+
62
+ when 'section'
63
+ (@self['section'])? @self['section'] : 'Others'
64
+
59
65
  when 'name', 'reminder', 'task' # Aliases
60
66
  @self['title']
61
67
 
@@ -67,11 +73,13 @@ module ICalPal
67
73
  super
68
74
 
69
75
  # Convert JSON arrays to Arrays
70
- @self['tags'] = JSON.parse(obj['tags']) if obj['tags']
71
- @self['location'] = JSON.parse(obj['location']).compact.uniq[0] if obj['location']
72
- @self['proximity'] = JSON.parse(obj['proximity']).compact.uniq[0] if obj['proximity']
73
- @self['radius'] = JSON.parse(obj['radius']).compact.uniq[0] if obj['radius']
74
- @self['assignee'] = JSON.parse(obj['assignee']) if obj['assignee']
76
+ %w[ assignee tags ].each do |a|
77
+ @self[a] = JSON.parse(obj[a]) if obj[a]
78
+ end
79
+
80
+ %w[ location proximity radius ].each do |a|
81
+ @self[a] = JSON.parse(obj[a]).compact.uniq[0] if obj[a]
82
+ end
75
83
 
76
84
  # Section
77
85
  if @self['members']
@@ -141,6 +149,30 @@ module ICalPal
141
149
  @self['messaging'] = messaging
142
150
  end
143
151
 
152
+ # @see ICalPal.<=>
153
+ #
154
+ # When comparing sections, "Others" always goes last
155
+ def <=>(other)
156
+ $sort_attrs.each do |s|
157
+ next if self[s] == other[s]
158
+
159
+ # nil is always less than
160
+ return -1 if other[s].nil?
161
+ return 1 if self[s].nil?
162
+
163
+ if s == 'section'
164
+ # Section "Others" always goes last
165
+ return -1 if other[s] == 'Others'
166
+ return 1 if self[s] == 'Others'
167
+ end
168
+
169
+ return -1 if self[s] < other[s]
170
+ return 1 if self[s] > other[s]
171
+ end
172
+
173
+ 0
174
+ end
175
+
144
176
  DEFAULT_COLOR = '#1BADF8'.freeze
145
177
  DEFAULT_SYMBOLIC_COLOR = 'blue'.freeze
146
178
 
data/lib/store.rb CHANGED
@@ -37,8 +37,6 @@ s1.type,
37
37
 
38
38
  FROM #{self.name.split('::').last} s1
39
39
 
40
- WHERE s1.delegated_account_owner_store_id IS NULL
41
-
42
40
  SQL
43
41
 
44
42
  end
data/lib/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module ICalPal
2
2
  NAME = 'icalPal'.freeze
3
- VERSION = '3.9.3'.freeze
3
+ VERSION = '3.10.0'.freeze
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icalPal
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.3
4
+ version: 3.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Rosen