icalPal 3.9.2 → 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: 726009d217b28c21e4a35339eef172f033429792b68d4c36157e6234f6221cab
4
- data.tar.gz: cf2ee63838a89a8acf25719154d03654e1bbf1fe47459481ddcb5e09f19a31f0
3
+ metadata.gz: 014f8b5179cd56cb757404df75a3d23145907d2f40b4186031ed5028ad5e92af
4
+ data.tar.gz: 8abe6a5c80c695fa61b0000ae20c52fcbd1fab29ab326fcb89fe1a4d469fae82
5
5
  SHA512:
6
- metadata.gz: 7120724aaa3be333484a6feca64cb1cf5b6f5ded5e93b5a4f6c99ec6e247ddd659bcc7c4f3af70b7b3e81dc28b1221bebac88d29c70bc497a89f242689ae088b
7
- data.tar.gz: b452c105b9ea954aa9c68a9b7a4088599b32b0b1b29e3f783804ec692eef65e8c7a1d916d3b9061c475ed880b950f9eebf31d870c8eda7751f33e4fd79bc9ae9
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,18 +29,25 @@ 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
- or in your home diretory:
38
+ For only you:
36
39
 
37
40
  ```
38
41
  gem install --user-install icalPal
39
42
  ```
40
43
 
44
+ As a [Homebrew](https://brew.sh) formula:
45
+
46
+ ```
47
+ brew tap ajrosen/tap
48
+ brew install icalPal
49
+ ```
50
+
41
51
  ## Features
42
52
 
43
53
  ### Compatability with icalBuddy
@@ -57,6 +67,8 @@ differences to be aware of.
57
67
 
58
68
  ### Additional commands
59
69
 
70
+ icalPal also supports additional commands.
71
+
60
72
  ```icalPal accounts```
61
73
 
62
74
  Shows a list of enabled Calendar accounts. Internally they are known
@@ -72,7 +84,7 @@ Shows only reminders that have a due date.
72
84
  *remindersDueBefore* can be used instead of *tasks*
73
85
 
74
86
  Reminders can also be viewed in the *Scheduled Reminders* calendar,
75
- using the *tasks* commands. Repeating reminders are treated the same
87
+ using the *events* commands. Repeating reminders are treated the same
76
88
  as repeating events.
77
89
 
78
90
  ### Additional options
@@ -91,39 +103,35 @@ as repeating events.
91
103
  * ```--color``` uses a wider color palette. Colors are what you have chosen in the Calendar and Reminders apps, including custom colors
92
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
93
105
 
94
- Because icalPal is written in Ruby, and not a native Mac application,
95
- you can run it just about anywhere. It's been tested with the
96
- versions of Ruby included with macOS Sequoia and Tahoe (2.6.10) and
97
- [Homebrew](https://brew.sh/) (3.4.x).
98
-
99
106
  ### Additional properties
100
107
 
101
108
  Several additional properties are available for each command.
102
109
 
103
110
  * Accounts
104
111
  * account
112
+ * delegations
105
113
  * notes
106
114
  * owner
107
115
  * type
108
- * delegations
109
116
 
110
117
  * Calendar
111
118
  * account
112
- * shared\_owner_name, shared\_owner_address
119
+ * locale
120
+ * notes
121
+ * published_URL
113
122
  * self\_identity_email, owner\_identity_email
123
+ * shared\_owner_name, shared\_owner_address
124
+ * sharees
114
125
  * subcal_account_id, subcal_url
115
- * published_URL
116
- * notes
117
- * locale
118
126
 
119
127
  * Tasks
120
- * id
121
- * grocery
128
+ * assignee
122
129
  * completed
130
+ * grocery
123
131
  * group
132
+ * id
124
133
  * section
125
134
  * tags
126
- * assignee
127
135
  * timezone
128
136
  * Notifications
129
137
  * due (due_date formatted with --df and --tf options)
@@ -136,7 +144,7 @@ Several additional properties are available for each command.
136
144
  icalPal: Usage: icalPal [options] [-c] COMMAND
137
145
 
138
146
  COMMAND must be one of the following:
139
- ```
147
+
140
148
  events Print events
141
149
  tasks Print tasks
142
150
  calendars Print calendars
@@ -152,29 +160,27 @@ COMMAND must be one of the following:
152
160
 
153
161
  stores can be used instead of accounts
154
162
  reminders can be used instead of tasks
155
- ```
156
163
 
157
164
  Global options:
158
- ```
165
+
159
166
  -c, --cmd=COMMAND Command to run
160
167
  --db=DB Use DB file instead of Calendar
161
- (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"]
162
169
  For the tasks commands this should be a directory containing .sqlite files
163
- (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"])
164
171
  --cf=FILE Set config file path (default: $HOME/.icalpal)
165
172
  --norc Ignore ICALPAL and ICALPAL_CONFIG environment variables
166
173
  -o, --output=FORMAT Print as FORMAT (default: default)
167
174
  [ansi, csv, default, hash, html, json, md, rdoc, remind, toc, xml, yaml]
168
- ```
169
175
 
170
- Including/excluding accounts, calendars, reminders and items:
171
- ```
176
+ Including/excluding accounts, calendars, items:
177
+
172
178
  --is=ACCOUNTS List of accounts to include
173
179
  --es=ACCOUNTS List of accounts to exclude
174
180
 
175
181
  --it=TYPES List of calendar types to include
176
182
  --et=TYPES List of calendar types to exclude
177
- [Local, Exchange, CalDAV, MobileMe, Subscribed, Birthdays]
183
+ [Local, Exchange, CalDAV, MobileMe, Subscribed, Birthdays, Reminders]
178
184
 
179
185
  --ic=CALENDARS List of calendars to include
180
186
  --ec=CALENDARS List of calendars to exclude
@@ -185,11 +191,10 @@ Including/excluding accounts, calendars, reminders and items:
185
191
  --id Include completed reminders
186
192
  --ed Exclude uncompleted reminders
187
193
 
188
- --match=FIELD=REGEX Include only items whose FIELD matches REGEXP (ignoring case)
189
- ```
194
+ --match=FIELD=REGEX Include only items whose FIELD matches REGEX (ignoring case)
190
195
 
191
196
  Choosing dates:
192
- ```
197
+
193
198
  --from=DATE List events starting on or after DATE
194
199
  --to=DATE List events starting on or before DATE
195
200
  DATE can be yesterday, today, tomorrow, +N, -N, or anything accepted by DateTime.parse()
@@ -200,10 +205,9 @@ Choosing dates:
200
205
  --sed Show empty dates with --sd
201
206
  --ia Include only all-day events
202
207
  --ea Exclude all-day events
203
- ```
204
208
 
205
209
  Choose properties to include in the output:
206
- ```
210
+
207
211
  --iep=PROPERTIES List of properties to include
208
212
  --eep=PROPERTIES List of properties to exclude
209
213
  --aep=PROPERTIES List of properties to include in addition to the default list
@@ -224,10 +228,9 @@ Choose properties to include in the output:
224
228
 
225
229
  Use 'all' for PROPERTIES to include all available properties (except any listed in --eep)
226
230
  Use 'list' for PROPERTIES to list all available properties and exit
227
- ```
228
231
 
229
232
  Formatting the output:
230
- ```
233
+
231
234
  --li=N Show at most N items (default: 0 for no limit)
232
235
 
233
236
  --sc Separate by calendar
@@ -254,24 +257,21 @@ Formatting the output:
254
257
 
255
258
  -f Format output using standard ANSI colors
256
259
  --color Format output using a larger color palette
257
- ```
258
260
 
259
261
  Help:
260
- ```
262
+
261
263
  -h, --help Show this message
262
- -V, -v, --version Show version and exit (3.9.1)
264
+ -V, -v, --version Show version and exit (3.10.0)
263
265
  -d, --debug=LEVEL Set the logging level (default: warn)
264
266
  [debug, info, warn, error, fatal]
265
- ```
266
267
 
267
268
  Environment variables:
268
- ```
269
+
269
270
  ICALPAL Additional arguments
270
271
  ICALPAL_CONFIG Additional arguments from a file
271
272
  (default: $HOME/.icalpal)
272
273
 
273
274
  Do not quote or escape values. Options set in ICALPAL override ICALPAL_CONFIG. Options on the command line override ICALPAL.
274
- ```
275
275
 
276
276
  ## Output formats
277
277
 
@@ -288,35 +288,9 @@ properly formatted.
288
288
 
289
289
  Other formats such as ANSI, HTML, Markdown, RDoc, and TOC, use Ruby's
290
290
  [RDoc::Markup](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup.html)
291
- framework to build and render the items.
292
-
293
- Each item to be printed is a new
294
- [RDoc::Markup::Document](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Document.html).
295
-
296
- When using one of the _separate by_ options, a section header is added
297
- first. The section contains:
298
-
299
- * [RDoc::Markup::BlankLine](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/BlankLine.html)
300
- (unless this is the first section)
301
- * RDoc::Markup::Heading (level 1)
302
- * [RDoc::Markup::Rule](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Rule.html)
303
-
304
- The rest of the document is a series of
305
- [RDoc::Markup::List](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/List.html)
306
- objects, one for each of the item's properties:
307
-
308
- * [RDoc::Markup::List](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/List.html)
309
- * RDoc::Markup::Heading (level 2)
310
- * [RDoc::Markup::BlankLine](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/BlankLine.html)
311
- * [RDoc::Markup::ListItem](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/ListItem.html)
312
- * [RDoc::Markup::Paragraph](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Paragraph.html)
313
-
314
- The document will also include a number of
315
- [RDoc::Markup::Verbatim](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Verbatim.html)
316
- and
317
- [RDoc::Markup::Raw](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Raw.html)
318
- items. They are not included in the output, but are used to pass
319
- 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.
320
294
 
321
295
  ## History
322
296
 
@@ -334,9 +308,14 @@ Lawton](https://github.com/jimlawton) that it even compiles anymore.
334
308
  Instead of trying to understand and extend the existing code, I chose
335
309
  to start anew using my language of choice:
336
310
  [Ruby](https://www.ruby-lang.org). Using Ruby meant there is *much*
337
- 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
338
312
  is multi-platform.
339
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
+
340
319
  I won't pretend to understand **why** you would want to run this on
341
320
  Linux or Windows. But since icalPal is written in Ruby and gets its
342
321
  data directly from the Calendar and Reminders database files instead
data/bin/icalPal CHANGED
@@ -13,7 +13,7 @@ def r(gem)
13
13
  $stderr.puts "FATAL: icalPal is missing a dependency: #{gem}"
14
14
  $stderr.puts e
15
15
  $stderr.puts
16
- abort "Try installing with 'gem install --user-install #{gem}'"
16
+ abort "Try installing with 'gem install #{gem}'"
17
17
  end
18
18
  end
19
19
 
@@ -63,7 +63,7 @@ $items = [] # Items to be printed
63
63
  ##################################################
64
64
  # All kids love log!
65
65
 
66
- $log.info("Options: #{$opts}")
66
+ $log.info("Options:\n#{$opts.to_json}")
67
67
 
68
68
 
69
69
  ##################################################
@@ -90,39 +90,38 @@ $opts[:db].each do |db|
90
90
  $log.debug("Trying #{db}")
91
91
 
92
92
  if klass == ICalPal::Reminder
93
- begin
94
- # Load all .sqlite files
95
- $log.debug("Loading *.sqlite in #{db}")
96
- Dir.glob("#{db}/*.sqlite").each do |d|
97
- success = true
98
-
99
- rows = klass.load_data(d, klass::QUERY)
100
- $rows += rows
93
+ # Load all .sqlite files
94
+ $log.debug("Loading *.sqlite in #{db}")
95
+ Dir.glob("#{db}/*.sqlite").each do |d|
96
+ success = true
101
97
 
102
- sections = klass.load_data(d, klass::SECTIONS_QUERY)
103
- $sections += sections
98
+ rows = klass.load_data(d, klass::QUERY)
99
+ $rows += rows
104
100
 
105
- $log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
106
- rescue SQLite3::CantOpenException
107
- # Non-fatal exception, try the next one
108
- end
101
+ sections = klass.load_data(d, klass::SECTIONS_QUERY)
102
+ $sections += sections
109
103
 
110
- rescue Errno::EPERM
111
- # Probably need Full Disk Access
104
+ $log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
112
105
  end
113
106
  else
114
107
  # Load database
115
- begin
116
- rows = ICalPal.load_data(db, klass::QUERY)
117
- $rows += rows
108
+ rows = ICalPal.load_data(db, klass::QUERY)
109
+ $rows += rows
118
110
 
119
- success = true
111
+ success = true
120
112
 
121
- $log.info("Loaded #{rows.length} rows from #{db}")
122
- rescue SQLite3::CantOpenException
123
- # Non-fatal exception, try the next one
124
- end
113
+ $log.info("Loaded #{rows.length} rows from #{db}")
125
114
  end
115
+
116
+ rescue Errno::EPERM
117
+ # Probably need Full Disk Access
118
+
119
+ rescue SQLite3::CantOpenException
120
+ # Non-fatal exception, try the next one
121
+
122
+ rescue StandardError => e
123
+ # Log the error and (try to) continue
124
+ $log.error("#{db}: #{e.message}")
126
125
  end
127
126
 
128
127
  # Make sure we opened at least one database
@@ -153,6 +152,7 @@ unless success
153
152
  abort
154
153
  end
155
154
 
155
+ $log.debug("Loaded #{$rows.length} #{klass} items")
156
156
  $log.info("Window is #{$opts[:from]} to #{$opts[:to]}") if $opts[:from]
157
157
 
158
158
 
@@ -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")
@@ -290,7 +290,7 @@ unless mu
290
290
  when 'remind' then items.map { |i|
291
291
  "REM #{i['sdate'].strftime('%F AT %R')} " +
292
292
  "DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i} " +
293
- "MSG #{i['title'].gsub(/([[:cntrl:]])/) { |c| c.dump[1..-2] } }"
293
+ "MSG #{i['title'].gsub(/([[:cntrl:]])/) { |c| c.dump[1..-2] }}"
294
294
  }.join("\n")
295
295
  else abort "No formatter for #{$opts[:output]}"
296
296
  end
@@ -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
@@ -13,7 +13,7 @@ def r(gem)
13
13
  $stderr.puts "FATAL: icalPal is missing a dependency: #{gem}"
14
14
  $stderr.puts e
15
15
  $stderr.puts
16
- abort "Try installing with 'gem install --user-install #{gem}'"
16
+ abort "Try installing with 'gem install #{gem}'"
17
17
  end
18
18
  end
19
19
 
@@ -63,7 +63,7 @@ $items = [] # Items to be printed
63
63
  ##################################################
64
64
  # All kids love log!
65
65
 
66
- $log.info("Options: #{$opts}")
66
+ $log.info("Options:\n#{$opts.to_json}")
67
67
 
68
68
 
69
69
  ##################################################
@@ -90,39 +90,38 @@ $opts[:db].each do |db|
90
90
  $log.debug("Trying #{db}")
91
91
 
92
92
  if klass == ICalPal::Reminder
93
- begin
94
- # Load all .sqlite files
95
- $log.debug("Loading *.sqlite in #{db}")
96
- Dir.glob("#{db}/*.sqlite").each do |d|
97
- success = true
98
-
99
- rows = klass.load_data(d, klass::QUERY)
100
- $rows += rows
93
+ # Load all .sqlite files
94
+ $log.debug("Loading *.sqlite in #{db}")
95
+ Dir.glob("#{db}/*.sqlite").each do |d|
96
+ success = true
101
97
 
102
- sections = klass.load_data(d, klass::SECTIONS_QUERY)
103
- $sections += sections
98
+ rows = klass.load_data(d, klass::QUERY)
99
+ $rows += rows
104
100
 
105
- $log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
106
- rescue SQLite3::CantOpenException
107
- # Non-fatal exception, try the next one
108
- end
101
+ sections = klass.load_data(d, klass::SECTIONS_QUERY)
102
+ $sections += sections
109
103
 
110
- rescue Errno::EPERM
111
- # Probably need Full Disk Access
104
+ $log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
112
105
  end
113
106
  else
114
107
  # Load database
115
- begin
116
- rows = ICalPal.load_data(db, klass::QUERY)
117
- $rows += rows
108
+ rows = ICalPal.load_data(db, klass::QUERY)
109
+ $rows += rows
118
110
 
119
- success = true
111
+ success = true
120
112
 
121
- $log.info("Loaded #{rows.length} rows from #{db}")
122
- rescue SQLite3::CantOpenException
123
- # Non-fatal exception, try the next one
124
- end
113
+ $log.info("Loaded #{rows.length} rows from #{db}")
125
114
  end
115
+
116
+ rescue Errno::EPERM
117
+ # Probably need Full Disk Access
118
+
119
+ rescue SQLite3::CantOpenException
120
+ # Non-fatal exception, try the next one
121
+
122
+ rescue StandardError => e
123
+ # Log the error and (try to) continue
124
+ $log.error("#{db}: #{e.message}")
126
125
  end
127
126
 
128
127
  # Make sure we opened at least one database
@@ -153,6 +152,7 @@ unless success
153
152
  abort
154
153
  end
155
154
 
155
+ $log.debug("Loaded #{$rows.length} #{klass} items")
156
156
  $log.info("Window is #{$opts[:from]} to #{$opts[:to]}") if $opts[:from]
157
157
 
158
158
 
@@ -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")
@@ -290,7 +290,7 @@ unless mu
290
290
  when 'remind' then items.map { |i|
291
291
  "REM #{i['sdate'].strftime('%F AT %R')} " +
292
292
  "DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i} " +
293
- "MSG #{i['title'].gsub(/([[:cntrl:]])/) { |c| c.dump[1..-2] } }"
293
+ "MSG #{i['title'].gsub(/([[:cntrl:]])/) { |c| c.dump[1..-2] }}"
294
294
  }.join("\n")
295
295
  else abort "No formatter for #{$opts[:output]}"
296
296
  end
@@ -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/ext/extconf.rb CHANGED
@@ -7,7 +7,7 @@ begin
7
7
  Gem.path.each { |p| gemdir = p if File.writable? p }
8
8
 
9
9
  # Dependencies common to all environments
10
- dependencies = %w[ plist timezone ]
10
+ dependencies = %w[ plist tzinfo ]
11
11
 
12
12
  if RUBY_VERSION >= '3.4'
13
13
  # bigdecimal is not part of the default gems starting from Ruby 3.4.0.
@@ -33,15 +33,15 @@ begin
33
33
  # So neither environment can install the other's sqlite3 gem. We
34
34
  # must install sqlite3, but iff we are not building with macOS'
35
35
  # Ruby installation.
36
- dependencies.push('sqlite3')
36
+ dependencies.push('sqlite3')
37
37
  end
38
38
 
39
39
  di = Gem::DependencyInstaller.new(install_dir: gemdir)
40
40
  dependencies.each { |d| di.install(d) }
41
- rescue Exception => e
41
+ rescue Exception
42
42
  exit(1)
43
- end
43
+ end
44
44
 
45
- File.write("Makefile", "clean:\n\ttrue\ninstall:\n\ttrue")
45
+ File.write('Makefile', "clean:\n\ttrue\ninstall:\n\ttrue")
46
46
 
47
47
  exit(0)
data/lib/ToICalPal.rb CHANGED
@@ -76,7 +76,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
76
76
  def accept_list_start(_arg)
77
77
  return if @opts[:nb] || @item['placeholder']
78
78
 
79
- if @item['due_date'] && (@item['due_date']).between?(0, $now.to_i)
79
+ if @item['due_date'] && (@item['due_date']).between?(0, $nowto_i)
80
80
  # Use alert bullet for overdue items
81
81
  @res << "#{@opts[:ab]} "
82
82
  else
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
@@ -1,7 +1,8 @@
1
1
  # Does anybody really know what time it is?
2
- now = Time.now
3
- $now = ICalPal::RDT.from_time(now)
4
- $today = ICalPal::RDT.new(*$now.to_a[0..2] + [ 0, 0, 0 ])
2
+ $now = Time.now
3
+ $nowto_i = $now.to_i
4
+ $nowrdt = ICalPal::RDT.from_time($now)
5
+ $today = $nowrdt.day_start
5
6
 
6
7
  # Defaults
7
8
  $defaults = {
@@ -48,31 +49,6 @@ $defaults = {
48
49
  sort: 'prio',
49
50
  },
50
51
 
51
- undatedTasks: {
52
- dated: 1,
53
- db: [ ICalPal::Reminder::DB_PATH ],
54
- iep: %w[ title notes due priority ],
55
- ps: [ "\n " ],
56
- sort: 'prio',
57
- },
58
-
59
- datedTasks: {
60
- dated: 2,
61
- db: [ ICalPal::Reminder::DB_PATH ],
62
- iep: %w[ title notes due priority ],
63
- ps: [ "\n " ],
64
- sort: 'prio',
65
- },
66
-
67
- tasksDueBefore: {
68
- dated: 3,
69
- db: [ ICalPal::Reminder::DB_PATH ],
70
- iep: %w[ title notes due priority ],
71
- ps: [ "\n " ],
72
- sort: 'prio',
73
- to: $today,
74
- },
75
-
76
52
  accounts: {
77
53
  iep: %w[ account type ],
78
54
  sort: 'account',
data/lib/event.rb CHANGED
@@ -1,5 +1,3 @@
1
- r 'timezone'
2
-
3
1
  module ICalPal
4
2
  # Class representing items from the <tt>CalendarItem</tt> table
5
3
  class Event
@@ -13,19 +11,19 @@ module ICalPal
13
11
  def []=(k, v)
14
12
  @self[k] = v
15
13
 
16
- @self['sctime'] = Time.at(@self['sdate'].to_i, in: '+00:00') if k == 'sdate'
17
- @self['ectime'] = Time.at(@self['edate'].to_i, in: '+00:00') if k == 'edate'
14
+ @self['sctime'] = Time.at(@self['sdate'].to_i) if k == 'sdate'
15
+ @self['ectime'] = Time.at(@self['edate'].to_i) if k == 'edate'
18
16
  end
19
17
 
20
18
  # Standard accessor with special handling for +age+,
21
- # +availability+, +datetime+, +location+, +notes+, +status+,
22
- # +title+, and +uid+
19
+ # +availability+, +datetime+, +location+, +notes+, +sday+,
20
+ # +status+, +uid+, and +event+/+name+/+title+
23
21
  #
24
22
  # @param k [String] Key/property name
25
23
  def [](k)
26
24
  case k
27
25
  when 'age' # pseudo-property
28
- @self['sdate'].year - @self['edate'].year
26
+ @self['sdate'].year - Time.at(@self['start_date'] + ITIME).year
29
27
 
30
28
  when 'availability' # Integer -> String
31
29
  EventKit::EKEventAvailability.select { |_k, v| v == @self['availability'] }.keys
@@ -50,17 +48,17 @@ module ICalPal
50
48
  (@self['notes'])? @self['notes'].strip.gsub("\n", $opts[:nnr]) : nil
51
49
 
52
50
  when 'sday' # pseudo-property
53
- RDT.new(*@self['sdate'].to_a[0..2])
51
+ @self['sdate'].day_start(0)
54
52
 
55
53
  when 'status' # Integer -> String
56
54
  EventKit::EKEventStatus.select { |_k, v| v == @self['status'] }.keys[0]
57
55
 
58
- when 'event', 'name', 'title' # title[ (age N)]
59
- @self['title'] + ((@self['calendar'] == 'Birthdays')? " (age #{self['age']})" : '')
60
-
61
56
  when 'uid' # for icalBuddy
62
57
  @self['UUID']
63
58
 
59
+ when 'event', 'name', 'title' # title[ (age N)]
60
+ @self['title'] + ((@self['calendar'] == 'Birthdays')? " (age #{self['age']})" : '')
61
+
64
62
  else @self[k]
65
63
  end
66
64
  end
@@ -78,7 +76,7 @@ module ICalPal
78
76
  'sdate' => obj,
79
77
  'placeholder' => true,
80
78
  'title' => 'Nothing.',
81
- } if obj.is_a?(DateTime)
79
+ } if $opts[:sed] && obj.is_a?(DateTime)
82
80
 
83
81
  super
84
82
 
@@ -92,17 +90,17 @@ module ICalPal
92
90
  obj.keys.select { |i| i.end_with? '_date' }.each do |k|
93
91
  next unless obj[k]
94
92
 
95
- begin
96
- zone = Timezone.fetch(obj['start_tz'])
97
- rescue Timezone::Error::InvalidZone
98
- zone = '+00:00'
99
- end
93
+ zone = nil
94
+ zone = '+00:00' if obj['all_day'].positive?
100
95
 
101
96
  # Save as seconds, Time, RDT
102
97
  ctime = obj[k] + ITIME
98
+ ctime -= Time.at(ctime).utc_offset if obj["#{k}_tz"] == '_float' && !zone
99
+ ttime = Time.at(ctime, in: zone)
100
+
103
101
  @self["#{k[0]}seconds"] = ctime
104
- @self["#{k[0]}ctime"] = Time.at(ctime)
105
- @self["#{k[0]}date"] = RDT.from_time(Time.at(ctime, in: zone))
102
+ @self["#{k[0]}ctime"] = ttime
103
+ @self["#{k[0]}date"] = RDT.from_time(ttime)
106
104
  end
107
105
 
108
106
  @self.delete('unique_identifier')
@@ -122,14 +120,14 @@ module ICalPal
122
120
  return events if nDays > 100_000
123
121
 
124
122
  # If multi-day, each (unique) day needs to end at 23:59:59
125
- self['edate'] = RDT.new(*@self['sdate'].to_a[0..2] + [ 23, 59, 59 ]) if nDays.positive?
123
+ self['edate'] = RDT.new(*@self['sdate'].to_a[0..2] + [ 23, 59, 59 ], @self['sdate'].zone) if nDays.positive?
126
124
 
127
125
  # Repeat for multi-day events
128
126
  (nDays + 1).times do |i|
129
- break if self['sdate'] > $opts[:to]
127
+ break unless $opts[:now] || @self['sdate'] <= $opts[:to]
130
128
 
131
- if in_window?(self['sdate'], self['edate'])
132
- self['daynum'] = i + 1 if nDays.positive?
129
+ if in_window?(@self['sdate'], @self['edate'])
130
+ @self['daynum'] = i + 1 if nDays.positive?
133
131
  events.push(clone)
134
132
  end
135
133
 
@@ -156,7 +154,7 @@ module ICalPal
156
154
  changes = [ { orig_date: -1 } ]
157
155
  changes += $rows.select { |r| r['orig_item_id'] == self['ROWID'] }
158
156
 
159
- while self['sdate'] <= stop
157
+ while @self['sdate'] <= stop
160
158
  # count
161
159
  break if self['count'].positive? && count > self['count']
162
160
 
@@ -178,7 +176,7 @@ module ICalPal
178
176
  skip = true if codate == odate
179
177
  end
180
178
 
181
- 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'])
182
180
  end
183
181
 
184
182
  # Handle frequency and interval
@@ -240,8 +238,8 @@ module ICalPal
240
238
  m = mo.to_i
241
239
 
242
240
  # Set dates to the first of <m>
243
- nsdate = RDT.new(self['sdate'].year, m, 1, self['sdate'].hour, self['sdate'].minute, self['sdate'].second)
244
- nedate = RDT.new(self['edate'].year, m, 1, self['edate'].hour, self['edate'].minute, self['edate'].second)
241
+ nsdate = RDT.new(@self['sdate'].year, m, 1, @self['sdate'].hour, @self['sdate'].min, @self['sdate'].sec, @self['sdate'].zone)
242
+ nedate = RDT.new(@self['edate'].year, m, 1, @self['edate'].hour, @self['edate'].min, @self['edate'].sec, @self['edate'].zone)
245
243
 
246
244
  # ...but not in the past
247
245
  nsdate >>= 12 if nsdate.month < m
@@ -266,11 +264,16 @@ module ICalPal
266
264
  c['sdate'] = ICalPal.nth(nth, day, nsdate)
267
265
  c['edate'] = ICalPal.nth(nth, day, nedate)
268
266
  else
269
- diff = day - c['sdate'].wday
270
- diff += 7 if diff.negative?
267
+ %w[ sdate edate ].each do |d|
268
+ diff = day - c['sdate'].wday
269
+ diff += 7 if diff.negative?
271
270
 
272
- c['sdate'] += diff
273
- c['edate'] += diff
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)
274
+
275
+ c[d] = RDT.from_time(t2)
276
+ end
274
277
  end
275
278
 
276
279
  o.push(clone(c)) if in_window?(c['sdate'], c['edate'])
@@ -282,18 +285,18 @@ module ICalPal
282
285
 
283
286
  # Apply frequency and interval
284
287
  def apply_frequency!
285
- # Leave edate alone for birthdays to compute age
286
- dates = [ 'sdate' ]
287
- dates << 'edate' unless self['calendar'].include?('Birthday')
288
-
289
- dates.each do |d|
288
+ %w[ sdate edate ].each do |d|
290
289
  case EventKit::EKRecurrenceFrequency[self['frequency'] - 1]
291
- when 'daily' then self[d] += self['interval']
292
- when 'weekly' then self[d] += self['interval'] * 7
293
- when 'monthly' then self[d] >>= self['interval']
294
- 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)
295
294
  else $log.error("Unknown frequency: #{self['frequency']}")
296
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)
297
300
  end
298
301
  end
299
302
 
@@ -305,15 +308,15 @@ module ICalPal
305
308
  # @return [Boolean]
306
309
  def in_window?(s, e)
307
310
  if $opts[:now]
308
- if ($now >= s && $now < e)
311
+ if $nowto_i.between?(s.to_i, e.to_i)
309
312
  $log.debug("now: #{s} to #{e} vs. #{$now}")
310
313
  true
311
314
  else
312
315
  $log.debug("not now: #{s} to #{e} vs. #{$now}")
313
316
  false
314
317
  end
315
- elsif ([ s, e ].max >= $opts[:from] && s < $opts[:to])
316
- $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]}")
317
320
  true
318
321
  else
319
322
  $log.debug("not in window: #{s} to #{e} vs. #{$opts[:from]} to #{$opts[:to]}")
@@ -340,6 +343,7 @@ CalendarItem.all_day,
340
343
  CalendarItem.availability,
341
344
  CalendarItem.conference_url_detected,
342
345
  CalendarItem.description AS notes,
346
+ CalendarItem.end_tz,
343
347
  CalendarItem.has_recurrences,
344
348
  CalendarItem.invitation_status,
345
349
  CalendarItem.orig_item_id,
data/lib/icalPal.rb CHANGED
@@ -40,6 +40,8 @@ module ICalPal
40
40
 
41
41
  # Prepare the query
42
42
  stmt = db.prepare(q)
43
+
44
+ # Check for "list" and "all" pseudo-properties
43
45
  abort(stmt.columns.sort.join(' ')) if $opts[:props].any? 'list'
44
46
  $opts[:props] = stmt.columns - $opts[:eep] if $opts[:props].any? 'all'
45
47
 
@@ -50,21 +52,6 @@ module ICalPal
50
52
  # Close the database
51
53
  db.close
52
54
  $log.debug("Closed #{db_file}")
53
-
54
- rescue SQLite3::BusyException => e
55
- $log.error("Non-fatal error closing database #{db.filename}")
56
- raise e
57
-
58
- rescue SQLite3::CantOpenException => e
59
- $log.debug("Can't open #{db_file}")
60
- raise e
61
-
62
- rescue SQLite3::SQLException => e
63
- $log.info("#{db_file}: #{e}")
64
- raise e
65
-
66
- rescue SQLite3::Exception => e
67
- abort("#{db_file}: #{e}")
68
55
  end
69
56
 
70
57
  rows
@@ -123,7 +110,7 @@ module ICalPal
123
110
  def self.nth(n, dow, m)
124
111
  # Get the number of days in the month by advancing to the first of
125
112
  # the next month, then going back one day
126
- a = [ RDT.new(m.year, m.month, 1, m.hour, m.minute, m.second) ]
113
+ a = [ RDT.new(m.year, m.month, 1, m.hour, m.min, m.sec, m.zone) ]
127
114
  a[1] = (a[0] >> 1) - 1
128
115
 
129
116
  # Reverse it if going backwards
@@ -137,7 +124,7 @@ module ICalPal
137
124
  end
138
125
  end
139
126
 
140
- # Epoch + 31 years
127
+ # Epoch + 31 years (Mon Jan 1 00:00:00 UTC 2001)
141
128
  ITIME = 978_307_200
142
129
 
143
130
  # Days of the week abbreviations used in recurrence rules
@@ -162,6 +149,25 @@ module ICalPal
162
149
  @self.values
163
150
  end
164
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
+
165
171
  # Like inspect, but easier for humans to read
166
172
  #
167
173
  # @return [Array<String>] @self as a key=value array, sorted by key
data/lib/options.rb CHANGED
@@ -239,7 +239,7 @@ module ICalPal
239
239
 
240
240
  @op.parse!(o, into: cf)
241
241
  rescue StandardError
242
- end unless cli[:norc]
242
+ end unless cli[:norc] && !cli[:cf]
243
243
 
244
244
  # Find command
245
245
  cli[:cmd] ||= @op.default_argv[0]
@@ -254,22 +254,43 @@ module ICalPal
254
254
  cli[:cmd] = cli[:cmd].sub('reminders', 'tasks')
255
255
  cli[:cmd] = cli[:cmd].sub('datedReminders', 'datedTasks')
256
256
 
257
- # Parse eventsNow and eventsToday commands
258
- cli[:cmd].match('events(Now|Today|Remaining)(\+[0-9]+)?') do |m|
259
- cli[:now] = true if m[1] == 'Now'
260
- cli[:days] = (m[1] == 'Today')? m[2].to_i : 1
257
+ # Handle events command variants
258
+ cli[:cmd].match('events(?<v>Now|Today|Remaining)(?<n>\+[0-9]+)?') do |m|
259
+ cli[:cmd] = 'events'
260
+
261
+ case m.named_captures['v']
262
+ when 'Now'
263
+ cli[:now] = true
261
264
 
262
- if m[1] == 'Remaining'
263
- cli[:n] = true
264
- cli[:days] = 0
265
+ when 'Today'
266
+ cli[:from] = $today
267
+ cli[:days] = (m.named_captures['n'])? m.named_captures['n'].to_i : 1
268
+
269
+ when 'Remaining'
270
+ cli[:from] = RDT.from_time($now)
271
+ cli[:to] = $today.day_end(0)
272
+ cli[:days] = 1
265
273
  end
274
+ end
266
275
 
267
- cli[:from] = $today
268
- cli[:to] = $today + cli[:days] if cli[:days]
269
- cli[:days] = Integer(cli[:to] - cli[:from])
276
+ # Handle tasks command variants
277
+ if cli[:cmd] =~ /tasks/i
278
+ case cli[:cmd]
279
+ when 'undatedTasks'
280
+ cli[:dated] = 1
270
281
 
271
- cli[:cmd] = 'events'
272
- end if cli[:cmd]
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'
293
+ end
273
294
 
274
295
  # Must have a valid command
275
296
  raise(OptionParser::InvalidArgument, "Unknown COMMAND #{cli[:cmd]}") unless (COMMANDS.any? cli[:cmd])
@@ -281,13 +302,6 @@ module ICalPal
281
302
  .merge(env)
282
303
  .merge(cli)
283
304
 
284
- # Other tasks commands
285
- if opts[:cmd] == 'tasksDueBefore'
286
- opts.delete(:days) unless opts[:days]
287
- opts[:from] = RDT.from_epoch(0) unless opts[:from]
288
- end
289
- opts[:cmd] = 'tasks' if %w[ datedTasks undatedTasks tasksDueBefore ].include? opts[:cmd]
290
-
291
305
  # Make sure opts[:db] and opts[:tasks] are Arrays
292
306
  opts[:db] = [ opts[:db] ] unless opts[:db].is_a?(Array)
293
307
  opts[:tasks] = [ opts[:tasks] ] unless opts[:db].is_a?(Array)
@@ -310,11 +324,19 @@ module ICalPal
310
324
  opts[:days] -= 1 if opts[:days]
311
325
 
312
326
  if opts[:from]
327
+ # -n
328
+ opts[:from] = RDT.from_time($now) if opts[:n]
329
+
330
+ # Default :to is :from + 1 day
331
+ # --days overrides
313
332
  opts[:to] ||= opts[:from] + 1 if opts[:from]
314
333
  opts[:to] = opts[:from] + opts[:days] if opts[:days]
315
- opts[:to] = RDT.new(*opts[:to].to_a[0..2] + [ 23, 59, 59 ])
334
+
335
+ # Make :to be end of day
336
+ opts[:to] = opts[:to].day_end
337
+
338
+ # Calculate days unless specified
316
339
  opts[:days] ||= Integer(opts[:to] - opts[:from])
317
- opts[:from] = $now if opts[:n]
318
340
  end
319
341
 
320
342
  # Sorting
@@ -348,7 +370,9 @@ module ICalPal
348
370
  end
349
371
 
350
372
  # Commands that can be run
351
- COMMANDS = %w[events eventsToday eventsNow eventsRemaining tasks datedTasks undatedTasks tasksDueBefore calendars accounts].freeze
373
+ COMMANDS = %w[events eventsToday eventsNow eventsRemaining
374
+ tasks datedTasks undatedTasks tasksDueBefore
375
+ calendars accounts].freeze
352
376
 
353
377
  # Supported output formats
354
378
  OUTFORMATS = %w[ansi csv default hash html json md rdoc remind toc xml yaml].freeze
data/lib/rdt.rb CHANGED
@@ -4,7 +4,7 @@ module ICalPal
4
4
 
5
5
  # Create a new RDT from a Time object
6
6
  def self.from_time(t)
7
- new(*t.to_a[0..5].reverse)
7
+ new(*t.to_a[0..5].reverse, (t.gmt_offset / 3600).to_s)
8
8
  end
9
9
 
10
10
  # Create a new RDT from seconds since epoch
@@ -42,8 +42,9 @@ module ICalPal
42
42
  # today.
43
43
  def to_s
44
44
  return strftime($opts[:df]) if $opts && $opts[:nrd] && $opts[:df]
45
+ return super unless $today && $opts
45
46
 
46
- case Integer(RDT.new(year, month, day) - $today)
47
+ case Integer(RDT.new(*ymd, month, day) - $today)
47
48
  when -2 then 'day before yesterday'
48
49
  when -1 then 'yesterday'
49
50
  when 0 then 'today'
@@ -67,17 +68,31 @@ module ICalPal
67
68
  to_time.to_i
68
69
  end
69
70
 
71
+ # @param [Integer] Optional UTC offset
72
+ # @return [RDT] Self at 00:00:00
73
+ def day_start(z = zone)
74
+ RDT.new(year, month, day, 0, 0, 0, z)
75
+ end
76
+
77
+ # @param [Integer] Optional UTC offset
78
+ # @return [RDT] Self at 23:59:59
79
+ def day_end(z = zone)
80
+ RDT.new(year, month, day, 23, 59, 59, z)
81
+ end
82
+
70
83
  # @return [Array] Only the year, month and day of self
71
84
  def ymd
72
85
  [ year, month, day ]
73
86
  end
74
87
 
75
- # @see ICalPal::RDT.to_s
76
- #
77
- # @return [Boolean]
78
- def ==(other)
79
- self.to_s == other.to_s
88
+ # @return [Array] Only the hour, min and sec of self
89
+ def hms
90
+ [ hour, min, sec ]
80
91
  end
81
92
 
93
+ # @return [Array] Only the year, month and day of self
94
+ def ymd
95
+ [ year, month, day ]
96
+ end
82
97
  end
83
98
  end
data/lib/reminder.rb CHANGED
@@ -1,4 +1,4 @@
1
- r 'timezone'
1
+ r 'tzinfo'
2
2
 
3
3
  module ICalPal
4
4
  # Class representing items from the <tt>Reminders</tt> database
@@ -8,6 +8,12 @@ module ICalPal
8
8
  def self.load_data(db_file, q)
9
9
  # Load items
10
10
  ICalPal.load_data(db_file, q)
11
+
12
+ rescue SQLite3::SQLException => e
13
+ # Data-local.sqlite does not have zremcdBaseList
14
+ raise e unless e.message =~ /no such table/
15
+
16
+ []
11
17
  end
12
18
 
13
19
  def [](k)
@@ -50,6 +56,12 @@ module ICalPal
50
56
  when 'sdate' # For sorting
51
57
  @self['due_date']
52
58
 
59
+ when 'sday'
60
+ @self['due'].day_start(0) if @self['due']
61
+
62
+ when 'section'
63
+ (@self['section'])? @self['section'] : 'Others'
64
+
53
65
  when 'name', 'reminder', 'task' # Aliases
54
66
  @self['title']
55
67
 
@@ -61,11 +73,13 @@ module ICalPal
61
73
  super
62
74
 
63
75
  # Convert JSON arrays to Arrays
64
- @self['tags'] = JSON.parse(obj['tags']) if obj['tags']
65
- @self['location'] = JSON.parse(obj['location']).compact.uniq[0] if obj['location']
66
- @self['proximity'] = JSON.parse(obj['proximity']).compact.uniq[0] if obj['proximity']
67
- @self['radius'] = JSON.parse(obj['radius']).compact.uniq[0] if obj['radius']
68
- @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
69
83
 
70
84
  # Section
71
85
  if @self['members']
@@ -91,8 +105,8 @@ module ICalPal
91
105
  if @self['due_date']
92
106
  begin
93
107
  @self['due_date'] += ITIME
94
- zone = Timezone.fetch(@self['timezone'])
95
- rescue Timezone::Error::InvalidZone
108
+ zone = TZInfo::Timezone.get(@self['timezone'])
109
+ rescue TZInfo::InvalidTimezoneIdentifier
96
110
  zone = '+00:00'
97
111
  end
98
112
 
@@ -135,6 +149,30 @@ module ICalPal
135
149
  @self['messaging'] = messaging
136
150
  end
137
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
+
138
176
  DEFAULT_COLOR = '#1BADF8'.freeze
139
177
  DEFAULT_SYMBOLIC_COLOR = 'blue'.freeze
140
178
 
@@ -169,15 +207,12 @@ bl1.zParentList as parent,
169
207
  bl1.zSharingStatus as shared,
170
208
  bl1.zShouldCategorizeGroceryItems as grocery,
171
209
 
172
- -- section members
173
210
  json(bl1.ZMembershipsOfRemindersInSectionsAsData) -> '$.memberships' AS members,
174
211
 
175
- -- group
176
212
  (SELECT zName
177
213
  FROM zremcdBaseList bl2
178
214
  WHERE bl2.z_pk = bl1.zParentList) AS 'group',
179
215
 
180
- -- location
181
216
  (SELECT json_group_array(zremcdObject.zTitle)
182
217
  FROM zremcdObject
183
218
  WHERE zremcdObject.z_pk IN (
@@ -186,7 +221,6 @@ WHERE zremcdObject.z_pk IN (
186
221
  WHERE zremcdObject.zReminder = r1.z_pk
187
222
  )) AS location,
188
223
 
189
- -- proximity
190
224
  (SELECT json_group_array(zremcdObject.zProximity)
191
225
  FROM zremcdObject
192
226
  WHERE zremcdObject.z_pk IN (
@@ -195,7 +229,6 @@ WHERE zremcdObject.z_pk IN (
195
229
  WHERE zremcdObject.zReminder = r1.z_pk
196
230
  )) AS proximity,
197
231
 
198
- -- radius
199
232
  (SELECT json_group_array(zremcdObject.zRadius)
200
233
  FROM zremcdObject
201
234
  WHERE zremcdObject.z_pk IN (
@@ -204,17 +237,15 @@ WHERE zremcdObject.z_pk IN (
204
237
  WHERE zremcdObject.zReminder = r1.z_pk
205
238
  )) AS radius,
206
239
 
207
- -- tags
208
240
  (SELECT json_group_array(zName)
209
241
  FROM zremcdHashtagLabel
210
242
  WHERE zremcdHashtagLabel.z_pk IN (
211
243
  SELECT zremcdObject.zHashtagLabel
212
244
  FROM zremcdObject
213
- JOIN zremcdreminder ON zremcdObject.zReminder3 = r1.z_pk
245
+ JOIN zremcdReminder ON zremcdObject.zReminder3 = r1.z_pk
214
246
  WHERE zremcdObject.zReminder3 = r1.z_pk
215
247
  )) AS tags,
216
248
 
217
- -- assignee
218
249
  (SELECT
219
250
  json_array(zNickname, zFirstName, zLastName, zAddress1)
220
251
  FROM zremcdObject
@@ -224,7 +255,6 @@ WHERE zremcdObject.z_pk IN (
224
255
  WHERE zReminder1 = r1.z_pk
225
256
  )) AS assignee,
226
257
 
227
- -- url
228
258
  (SELECT zURL
229
259
  FROM zremcdObject
230
260
  WHERE zReminder2 = r1.z_pk) AS url
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.2'.freeze
3
+ VERSION = '3.10.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.9.2
4
+ version: 3.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Rosen
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: |
13
13
  Inspired by icalBuddy and maintains close compatability. Includes
@@ -62,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
62
  - !ruby/object:Gem::Version
63
63
  version: '0'
64
64
  requirements: []
65
- rubygems_version: 3.6.6
65
+ rubygems_version: 3.7.2
66
66
  specification_version: 4
67
67
  summary: Command-line tool to query the macOS Calendar and Reminders
68
68
  test_files: []