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 +4 -4
- data/README.md +52 -73
- data/bin/icalPal +37 -35
- data/bin/icalpal +37 -35
- data/ext/extconf.rb +5 -5
- data/lib/ToICalPal.rb +1 -1
- data/lib/calendar.rb +12 -0
- data/lib/defaults.rb +4 -28
- data/lib/event.rb +47 -43
- data/lib/icalPal.rb +23 -17
- data/lib/options.rb +47 -23
- data/lib/rdt.rb +22 -7
- data/lib/reminder.rb +47 -17
- data/lib/store.rb +0 -2
- data/lib/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 014f8b5179cd56cb757404df75a3d23145907d2f40b4186031ed5028ad5e92af
|
|
4
|
+
data.tar.gz: 8abe6a5c80c695fa61b0000ae20c52fcbd1fab29ab326fcb89fe1a4d469fae82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24bafd17756e9e4ccce28dc0b23ea421d334f3d9c58f3fe62dd07cf26777b47c56587a52e61c5995a730b823a3ad2f0ebc8bef460205ebcde981df3e0bd35108
|
|
7
|
+
data.tar.gz: a21caa7650b168542625a176909e05e0f87825c452015e7292f5297aa8b9d96863dc22c1f921a197a97769c9b3fb5db0a593adf1b22784b1874e03ecb05a8b34
|
data/README.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
|
+
[](https://badge.fury.io/rb/icalPal.svg?icon=si%3Arubygems&icon_color=%23ff0000)
|
|
3
|
+
[](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
|
|
8
|
-
databases for accounts, calendars, events, and tasks. It
|
|
9
|
-
on any system with [Ruby](https://www.ruby-lang.org/) and
|
|
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
|
-
|
|
32
|
+
Install icalPal for all users:
|
|
30
33
|
|
|
31
34
|
```
|
|
32
35
|
gem install icalPal
|
|
33
36
|
```
|
|
34
37
|
|
|
35
|
-
|
|
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 *
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
98
|
+
rows = klass.load_data(d, klass::QUERY)
|
|
99
|
+
$rows += rows
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
$rows += rows
|
|
108
|
+
rows = ICalPal.load_data(db, klass::QUERY)
|
|
109
|
+
$rows += rows
|
|
118
110
|
|
|
119
|
-
|
|
111
|
+
success = true
|
|
120
112
|
|
|
121
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 #{
|
|
239
|
+
$log.info("Sorting #{$items.count} items by #{$sort_attrs}, reverse #{$opts[:reverse].inspect}")
|
|
240
240
|
|
|
241
|
-
$items.
|
|
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
|
-
|
|
325
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
98
|
+
rows = klass.load_data(d, klass::QUERY)
|
|
99
|
+
$rows += rows
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
$rows += rows
|
|
108
|
+
rows = ICalPal.load_data(db, klass::QUERY)
|
|
109
|
+
$rows += rows
|
|
118
110
|
|
|
119
|
-
|
|
111
|
+
success = true
|
|
120
112
|
|
|
121
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 #{
|
|
239
|
+
$log.info("Sorting #{$items.count} items by #{$sort_attrs}, reverse #{$opts[:reverse].inspect}")
|
|
240
240
|
|
|
241
|
-
$items.
|
|
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
|
-
|
|
325
|
-
|
|
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
|
|
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
|
|
41
|
+
rescue Exception
|
|
42
42
|
exit(1)
|
|
43
|
-
end
|
|
43
|
+
end
|
|
44
44
|
|
|
45
|
-
File.write(
|
|
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, $
|
|
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
|
-
$
|
|
4
|
-
$
|
|
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
|
|
17
|
-
@self['ectime'] = Time.at(@self['edate'].to_i
|
|
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+, +
|
|
22
|
-
# +
|
|
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['
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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"] =
|
|
105
|
-
@self["#{k[0]}date"] = RDT.from_time(
|
|
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
|
|
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'])
|
|
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'].
|
|
244
|
-
nedate = RDT.new(self['edate'].year, m, 1, self['edate'].hour, self['edate'].
|
|
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
|
-
|
|
270
|
-
|
|
267
|
+
%w[ sdate edate ].each do |d|
|
|
268
|
+
diff = day - c['sdate'].wday
|
|
269
|
+
diff += 7 if diff.negative?
|
|
271
270
|
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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]
|
|
292
|
-
when 'weekly' then self[d]
|
|
293
|
-
when 'monthly' then self[d]
|
|
294
|
-
when 'yearly' then self[d]
|
|
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 (
|
|
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]
|
|
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.
|
|
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
|
-
#
|
|
258
|
-
cli[:cmd].match('events(Now|Today|Remaining)(
|
|
259
|
-
cli[:
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
cli[:
|
|
264
|
-
cli[:days] =
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
# @
|
|
76
|
-
|
|
77
|
-
|
|
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 '
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
95
|
-
rescue
|
|
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
|
|
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
data/lib/version.rb
CHANGED
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.
|
|
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:
|
|
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.
|
|
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: []
|