icalPal 3.7.2 → 3.8.1
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 +47 -12
- data/bin/icalPal +25 -8
- data/bin/icalpal +25 -8
- data/lib/EventKit.rb +23 -0
- data/lib/calendar.rb +29 -8
- data/lib/defaults.rb +4 -1
- data/lib/event.rb +3 -9
- data/lib/icalPal.rb +20 -11
- data/lib/options.rb +8 -3
- data/lib/reminder.rb +183 -37
- data/lib/store.rb +31 -5
- data/lib/utils.rb +26 -0
- data/lib/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf84e60f7f554a927ec10ddf6bfded859e4e628b09741b2c70f91838a659d5a3
|
4
|
+
data.tar.gz: 94f77b590ed672d7917a69e11b731f340d811eb7566bd939148253b697df53b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6afce7eee696743c6fabfbee6c5473326a39d38e9e9765e6b762506f54c89ac1efd1313e8b427e01c8d1a2537a574dfbce0e29c2afb1264b086c978f1e126a2e
|
7
|
+
data.tar.gz: 251ad5ac116830f49fb87eddcf58f5c27397e38c5ed8fc132b859f58976000500223755cd499871c1fc2d7b484f6caa9e9b3361b3ba1da7b85902083680aed57
|
data/README.md
CHANGED
@@ -10,14 +10,13 @@ on any system with [Ruby](https://www.ruby-lang.org/) and access to a
|
|
10
10
|
Calendar or Reminders database.
|
11
11
|
|
12
12
|
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
|
13
|
-
|
14
13
|
**Table of Contents**
|
15
14
|
|
16
15
|
- [Installation](#installation)
|
17
16
|
- [Features](#features)
|
18
|
-
- [Compatability with icalBuddy](#compatability-with-icalbuddy)
|
19
17
|
- [Additional commands](#additional-commands)
|
20
18
|
- [Additional options](#additional-options)
|
19
|
+
- [Additional properties](#additional-properties)
|
21
20
|
- [Usage](#usage)
|
22
21
|
- [Output formats](#output-formats)
|
23
22
|
- [History](#history)
|
@@ -67,24 +66,61 @@ as *Stores*; you can run ```icalPal stores``` instead.
|
|
67
66
|
|
68
67
|
Shows only reminders that have a due date.
|
69
68
|
|
69
|
+
```icalPal reminders```
|
70
|
+
|
71
|
+
*reminders*, *datedReminders*, and *undatedReminders* can be used instead of *tasks*
|
72
|
+
|
70
73
|
### Additional options
|
71
74
|
|
72
75
|
* Options can be abbreviated, so long as they are unique. Eg., ```icalPal -c ev --da 3``` is the same as ```icalPal -c events --days 3```.
|
73
76
|
* The ```-c``` part is optional, but you cannot abbreviate the command if you leave it off.
|
74
77
|
* Use ```-o``` to print the output in different formats. CSV or JSON are intertesting choices.
|
75
|
-
* Copy your Calendar database file and use ```--db``` on it.
|
78
|
+
* Copy your Calendar or Reminders database file and use ```--db``` on it.
|
76
79
|
* ```--it``` and ```--et``` will filter by Calendar *type*. Types are **Local**, **Exchange**, **CalDAV**, **MobileMe**, **Subscribed**, **Birthdays**, and **Reminders**
|
77
80
|
* ```--il``` and ```-el``` will filter by Reminder list
|
78
81
|
* ```--ia``` includes *only* all-day events (opposite of ```--ea```)
|
79
82
|
* ```--aep``` is like ```--iep```, but *adds* to the default property list instead of replacing it.
|
80
83
|
* ```--sep``` to separate by any property, not just calendar (```--sc```) or date (```--sd```)
|
81
|
-
* ```--color``` uses a wider color palette.
|
84
|
+
* ```--color``` uses a wider color palette. Colors are what you have chosen in the Calendar and Reminders apps, including custom colors
|
82
85
|
* ```--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
|
83
86
|
|
84
87
|
Because icalPal is written in Ruby, and not a native Mac application,
|
85
88
|
you can run it just about anywhere. It's been tested with the
|
86
|
-
versions of Ruby included with macOS Sequoia (2.6.10) and
|
87
|
-
[Homebrew](https://brew.sh/) (3.4.
|
89
|
+
versions of Ruby included with macOS Sequoia and Tahoe (2.6.10) and
|
90
|
+
[Homebrew](https://brew.sh/) (3.4.x).
|
91
|
+
|
92
|
+
### Additional properties
|
93
|
+
|
94
|
+
Several additional properties are available for each command.
|
95
|
+
|
96
|
+
* Accounts
|
97
|
+
* account
|
98
|
+
* notes
|
99
|
+
* owner
|
100
|
+
* type
|
101
|
+
* delegations
|
102
|
+
|
103
|
+
* Calendar
|
104
|
+
* account
|
105
|
+
* shared\_owner_name, shared\_owner_address
|
106
|
+
* self\_identity_email, owner\_identity_email
|
107
|
+
* subcal_account_id, subcal_url
|
108
|
+
* published_URL
|
109
|
+
* notes
|
110
|
+
* locale
|
111
|
+
|
112
|
+
* Tasks
|
113
|
+
* id
|
114
|
+
* group
|
115
|
+
* section
|
116
|
+
* tags
|
117
|
+
* assignee
|
118
|
+
* timezone
|
119
|
+
* Notifications
|
120
|
+
* due (due_date formatted with --df and --tf options)
|
121
|
+
* alert (Early Reminder)
|
122
|
+
* location, proximity (arriving or leaving), radius (in meters)
|
123
|
+
* messaging (email addresses and phone numbers from "When Messaging")
|
88
124
|
|
89
125
|
## Usage
|
90
126
|
|
@@ -109,16 +145,16 @@ Global options:
|
|
109
145
|
```
|
110
146
|
-c, --cmd=COMMAND Command to run
|
111
147
|
--db=DB Use DB file instead of Calendar
|
112
|
-
(default: ["/
|
148
|
+
(default: ["$HOME/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb", "/Users/user/Library/Calendars/Calendar.sqlitedb"]
|
113
149
|
For the tasks commands this should be a directory containing .sqlite files
|
114
|
-
(default: /
|
115
|
-
--cf=FILE Set config file path (default:
|
150
|
+
(default: $HOME/Library/Group Containers/group.com.apple.reminders/Container_v1/Stores)
|
151
|
+
--cf=FILE Set config file path (default: $HOME/.icalpal)
|
116
152
|
--norc Ignore ICALPAL and ICALPAL_CONFIG environment variables
|
117
153
|
-o, --output=FORMAT Print as FORMAT (default: default)
|
118
154
|
[ansi, csv, default, hash, html, json, md, rdoc, remind, toc, xml, yaml]
|
119
155
|
```
|
120
156
|
|
121
|
-
Including/excluding calendars and
|
157
|
+
Including/excluding accounts, calendars, reminders and items:
|
122
158
|
```
|
123
159
|
--is=ACCOUNTS List of accounts to include
|
124
160
|
--es=ACCOUNTS List of accounts to exclude
|
@@ -133,8 +169,7 @@ Including/excluding calendars and reminders:
|
|
133
169
|
--il=LISTS List of reminder lists to include
|
134
170
|
--el=LISTS List of reminder lists to exclude
|
135
171
|
|
136
|
-
--match=FIELD=REGEXP
|
137
|
-
Include only items whose FIELD matches REGEXP (ignoring case)
|
172
|
+
--match=FIELD=REGEX Include only items whose FIELD matches REGEXP (ignoring case)
|
138
173
|
```
|
139
174
|
|
140
175
|
Choosing dates:
|
data/bin/icalPal
CHANGED
@@ -56,6 +56,7 @@ end
|
|
56
56
|
$opts = ICalPal::Options.new.parse_options
|
57
57
|
|
58
58
|
$rows = [] # Rows from the database
|
59
|
+
$sections = [] # Calendar list sections
|
59
60
|
$items = [] # Items to be printed
|
60
61
|
|
61
62
|
|
@@ -93,19 +94,31 @@ $opts[:db].each do |db|
|
|
93
94
|
# Load all .sqlite files
|
94
95
|
$log.debug("Loading *.sqlite in #{db}")
|
95
96
|
Dir.glob("#{db}/*.sqlite").each do |d|
|
96
|
-
$rows += ICalPal.load_data(d, klass::QUERY)
|
97
97
|
success = true
|
98
98
|
|
99
|
+
rows = klass.load_data(d, klass::QUERY)
|
100
|
+
$rows += rows
|
101
|
+
|
102
|
+
sections = klass.load_data(d, klass::SECTIONS_QUERY)
|
103
|
+
$sections += sections
|
104
|
+
|
105
|
+
$log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
|
99
106
|
rescue SQLite3::CantOpenException
|
100
107
|
# Non-fatal exception, try the next one
|
101
108
|
end
|
109
|
+
|
110
|
+
rescue Errno::EPERM
|
111
|
+
# Probably need Full Disk Access
|
102
112
|
end
|
103
113
|
else
|
104
114
|
# Load database
|
105
115
|
begin
|
106
|
-
|
116
|
+
rows = ICalPal.load_data(db, klass::QUERY)
|
117
|
+
$rows += rows
|
118
|
+
|
107
119
|
success = true
|
108
120
|
|
121
|
+
$log.info("Loaded #{rows.length} rows from #{db}")
|
109
122
|
rescue SQLite3::CantOpenException
|
110
123
|
# Non-fatal exception, try the next one
|
111
124
|
end
|
@@ -140,8 +153,7 @@ unless success
|
|
140
153
|
abort
|
141
154
|
end
|
142
155
|
|
143
|
-
$log.info("
|
144
|
-
$log.info("Window is #{$opts[:from]} to #{$opts[:to]}")
|
156
|
+
$log.info("Window is #{$opts[:from]} to #{$opts[:to]}") if $opts[:from]
|
145
157
|
|
146
158
|
|
147
159
|
##################################################
|
@@ -191,8 +203,8 @@ $rows.each do |row|
|
|
191
203
|
else
|
192
204
|
# Check for dated reminders
|
193
205
|
if ICalPal::Reminder === item
|
194
|
-
next if $opts[:dated] == 1 && item['due_date'].positive?
|
195
|
-
next if $opts[:dated] == 2 && item['due_date'].zero?
|
206
|
+
next if $opts[:dated] == 1 && item['due_date'] && item['due_date'].positive?
|
207
|
+
next if $opts[:dated] == 2 && (!item['due_date'] || item['due_date'].zero?)
|
196
208
|
end
|
197
209
|
|
198
210
|
add(item)
|
@@ -211,9 +223,14 @@ end
|
|
211
223
|
|
212
224
|
# Sort the rows
|
213
225
|
begin
|
214
|
-
|
226
|
+
sort = []
|
227
|
+
sort.push $opts[:sep] if $opts[:sep]
|
228
|
+
sort.push $opts[:sort] if $opts[:sort]
|
229
|
+
sort.push 'sdate'
|
230
|
+
|
231
|
+
$log.info("Sorting #{$items.count} items by #{sort}, reverse #{$opts[:reverse].inspect}")
|
215
232
|
|
216
|
-
$items.sort_by! { |i| [
|
233
|
+
$items.sort_by! { |i| [i[sort[0]], i[sort[1]], i[sort[2]] ] }
|
217
234
|
$items.reverse! if $opts[:reverse]
|
218
235
|
rescue Exception => e
|
219
236
|
$log.info("Sorting failed: #{e}\n")
|
data/bin/icalpal
CHANGED
@@ -56,6 +56,7 @@ end
|
|
56
56
|
$opts = ICalPal::Options.new.parse_options
|
57
57
|
|
58
58
|
$rows = [] # Rows from the database
|
59
|
+
$sections = [] # Calendar list sections
|
59
60
|
$items = [] # Items to be printed
|
60
61
|
|
61
62
|
|
@@ -93,19 +94,31 @@ $opts[:db].each do |db|
|
|
93
94
|
# Load all .sqlite files
|
94
95
|
$log.debug("Loading *.sqlite in #{db}")
|
95
96
|
Dir.glob("#{db}/*.sqlite").each do |d|
|
96
|
-
$rows += ICalPal.load_data(d, klass::QUERY)
|
97
97
|
success = true
|
98
98
|
|
99
|
+
rows = klass.load_data(d, klass::QUERY)
|
100
|
+
$rows += rows
|
101
|
+
|
102
|
+
sections = klass.load_data(d, klass::SECTIONS_QUERY)
|
103
|
+
$sections += sections
|
104
|
+
|
105
|
+
$log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
|
99
106
|
rescue SQLite3::CantOpenException
|
100
107
|
# Non-fatal exception, try the next one
|
101
108
|
end
|
109
|
+
|
110
|
+
rescue Errno::EPERM
|
111
|
+
# Probably need Full Disk Access
|
102
112
|
end
|
103
113
|
else
|
104
114
|
# Load database
|
105
115
|
begin
|
106
|
-
|
116
|
+
rows = ICalPal.load_data(db, klass::QUERY)
|
117
|
+
$rows += rows
|
118
|
+
|
107
119
|
success = true
|
108
120
|
|
121
|
+
$log.info("Loaded #{rows.length} rows from #{db}")
|
109
122
|
rescue SQLite3::CantOpenException
|
110
123
|
# Non-fatal exception, try the next one
|
111
124
|
end
|
@@ -140,8 +153,7 @@ unless success
|
|
140
153
|
abort
|
141
154
|
end
|
142
155
|
|
143
|
-
$log.info("
|
144
|
-
$log.info("Window is #{$opts[:from]} to #{$opts[:to]}")
|
156
|
+
$log.info("Window is #{$opts[:from]} to #{$opts[:to]}") if $opts[:from]
|
145
157
|
|
146
158
|
|
147
159
|
##################################################
|
@@ -191,8 +203,8 @@ $rows.each do |row|
|
|
191
203
|
else
|
192
204
|
# Check for dated reminders
|
193
205
|
if ICalPal::Reminder === item
|
194
|
-
next if $opts[:dated] == 1 && item['due_date'].positive?
|
195
|
-
next if $opts[:dated] == 2 && item['due_date'].zero?
|
206
|
+
next if $opts[:dated] == 1 && item['due_date'] && item['due_date'].positive?
|
207
|
+
next if $opts[:dated] == 2 && (!item['due_date'] || item['due_date'].zero?)
|
196
208
|
end
|
197
209
|
|
198
210
|
add(item)
|
@@ -211,9 +223,14 @@ end
|
|
211
223
|
|
212
224
|
# Sort the rows
|
213
225
|
begin
|
214
|
-
|
226
|
+
sort = []
|
227
|
+
sort.push $opts[:sep] if $opts[:sep]
|
228
|
+
sort.push $opts[:sort] if $opts[:sort]
|
229
|
+
sort.push 'sdate'
|
230
|
+
|
231
|
+
$log.info("Sorting #{$items.count} items by #{sort}, reverse #{$opts[:reverse].inspect}")
|
215
232
|
|
216
|
-
$items.sort_by! { |i| [
|
233
|
+
$items.sort_by! { |i| [i[sort[0]], i[sort[1]], i[sort[2]] ] }
|
217
234
|
$items.reverse! if $opts[:reverse]
|
218
235
|
rescue Exception => e
|
219
236
|
$log.info("Sorting failed: #{e}\n")
|
data/lib/EventKit.rb
CHANGED
@@ -37,6 +37,29 @@ class EventKit
|
|
37
37
|
'low', # 9
|
38
38
|
].freeze
|
39
39
|
|
40
|
+
EKReminderDueDateDeltaUnit = %w[
|
41
|
+
minutes
|
42
|
+
hours
|
43
|
+
days
|
44
|
+
weeks
|
45
|
+
months
|
46
|
+
].freeze
|
47
|
+
|
48
|
+
EKReminderProximity = [
|
49
|
+
nil,
|
50
|
+
'arriving',
|
51
|
+
'leaving'
|
52
|
+
].freeze
|
53
|
+
|
54
|
+
EKReminderAccessLevel = [
|
55
|
+
{ value: 2, level: 'can add' }
|
56
|
+
].freeze
|
57
|
+
|
58
|
+
EKReminderSharingStatus = [
|
59
|
+
{ value: 1, status: 'accepted' },
|
60
|
+
{ value: 5, status: 'invited' }
|
61
|
+
].freeze
|
62
|
+
|
40
63
|
# EKSourceType (with color)
|
41
64
|
EKSourceType = [
|
42
65
|
{ name: 'Local', color: '#FFFFFF' }, # White
|
data/lib/calendar.rb
CHANGED
@@ -3,20 +3,41 @@ module ICalPal
|
|
3
3
|
class Calendar
|
4
4
|
include ICalPal
|
5
5
|
|
6
|
+
def [](k)
|
7
|
+
case k
|
8
|
+
when 'name', 'title' # Aliases
|
9
|
+
@self['calendar']
|
10
|
+
|
11
|
+
else @self[k]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
6
15
|
QUERY = <<~SQL.freeze
|
7
16
|
SELECT DISTINCT
|
8
17
|
|
9
|
-
|
10
|
-
|
11
|
-
|
18
|
+
s1.name AS account,
|
19
|
+
|
20
|
+
c1.UUID,
|
21
|
+
c1.title AS calendar,
|
22
|
+
|
23
|
+
c1.shared_owner_name,
|
24
|
+
c1.shared_owner_address,
|
25
|
+
|
26
|
+
c1.published_URL,
|
27
|
+
c1.self_identity_email,
|
28
|
+
c1.owner_identity_email,
|
29
|
+
c1.notes,
|
30
|
+
c1.subcal_account_id,
|
31
|
+
c1.subcal_url,
|
32
|
+
c1.locale
|
12
33
|
|
13
|
-
FROM #{self.name.split('::').last}
|
34
|
+
FROM #{self.name.split('::').last} c1
|
14
35
|
|
15
|
-
JOIN Store ON store_id =
|
36
|
+
JOIN Store s1 ON c1.store_id = s1.rowid
|
16
37
|
|
17
|
-
WHERE
|
18
|
-
AND
|
19
|
-
AND
|
38
|
+
WHERE s1.disabled IS NOT 1
|
39
|
+
AND s1.display_order IS NOT -1
|
40
|
+
AND c1.flags IS NOT 519
|
20
41
|
SQL
|
21
42
|
|
22
43
|
end
|
data/lib/defaults.rb
CHANGED
@@ -44,6 +44,7 @@ $defaults = {
|
|
44
44
|
dated: 0,
|
45
45
|
db: [ ICalPal::Reminder::DB_PATH ],
|
46
46
|
iep: %w[ title notes due priority ],
|
47
|
+
ps: [ "\n " ],
|
47
48
|
sort: 'prio',
|
48
49
|
},
|
49
50
|
|
@@ -51,6 +52,7 @@ $defaults = {
|
|
51
52
|
dated: 1,
|
52
53
|
db: [ ICalPal::Reminder::DB_PATH ],
|
53
54
|
iep: %w[ title notes due priority ],
|
55
|
+
ps: [ "\n " ],
|
54
56
|
sort: 'prio',
|
55
57
|
},
|
56
58
|
|
@@ -58,10 +60,11 @@ $defaults = {
|
|
58
60
|
dated: 2,
|
59
61
|
db: [ ICalPal::Reminder::DB_PATH ],
|
60
62
|
iep: %w[ title notes due priority ],
|
63
|
+
ps: [ "\n " ],
|
61
64
|
sort: 'prio',
|
62
65
|
},
|
63
66
|
|
64
|
-
|
67
|
+
accounts: {
|
65
68
|
iep: %w[ account type ],
|
66
69
|
sort: 'account',
|
67
70
|
},
|
data/lib/event.rb
CHANGED
@@ -55,7 +55,7 @@ module ICalPal
|
|
55
55
|
when 'status' # Integer -> String
|
56
56
|
EventKit::EKEventStatus.select { |_k, v| v == @self['status'] }.keys[0]
|
57
57
|
|
58
|
-
when 'title'
|
58
|
+
when 'event', 'name', 'title' # title[ (age N)]
|
59
59
|
@self['title'] + ((@self['calendar'] == 'Birthdays')? " (age #{self['age']})" : '')
|
60
60
|
|
61
61
|
when 'uid' # for icalBuddy
|
@@ -80,8 +80,7 @@ module ICalPal
|
|
80
80
|
'title' => 'Nothing.',
|
81
81
|
} if DateTime === obj
|
82
82
|
|
83
|
-
|
84
|
-
obj.each_key { |k| @self[k] = obj[k] }
|
83
|
+
super
|
85
84
|
|
86
85
|
# Convert JSON arrays to Arrays
|
87
86
|
@self['attendees'] = JSON.parse(obj['attendees'])
|
@@ -106,12 +105,7 @@ module ICalPal
|
|
106
105
|
@self["#{k[0]}date"] = RDT.from_time(Time.at(ctime, in: zone))
|
107
106
|
end
|
108
107
|
|
109
|
-
|
110
|
-
obj['type'] = EventKit::EKSourceType.find_index { |i| i[:name] == 'Subscribed' } if obj['subcal_url']
|
111
|
-
type = EventKit::EKSourceType[obj['type']]
|
112
|
-
|
113
|
-
@self['symbolic_color_name'] ||= @self['color']
|
114
|
-
@self['type'] = type[:name]
|
108
|
+
@self.delete('unique_identifier')
|
115
109
|
end
|
116
110
|
|
117
111
|
# Check non-recurring events
|
data/lib/icalPal.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
%w[ EventKit ToICalPal calendar event rdt reminder store ].each { |l| rr l }
|
2
2
|
|
3
3
|
# Encapsulate the _Store_ (accounts), _Calendar_ and _CalendarItem_
|
4
|
-
# tables of a Calendar database, and
|
4
|
+
# tables of a Calendar database, and _Reminders_ (tasks) of a
|
5
5
|
# Reminders database
|
6
6
|
module ICalPal
|
7
7
|
attr_reader :self
|
@@ -9,11 +9,11 @@ module ICalPal
|
|
9
9
|
# Dynamic instantiation of our classes based on the command being
|
10
10
|
# run
|
11
11
|
#
|
12
|
-
# @param klass [String] One of +accounts+, +
|
12
|
+
# @param klass [String] One of +accounts+, +calendars+, +events+, or +tasks+
|
13
13
|
# @return [Class] The subclass of ICalPal
|
14
14
|
def self.call(klass)
|
15
15
|
case klass
|
16
|
-
when 'accounts'
|
16
|
+
when 'accounts' then Store
|
17
17
|
when 'calendars' then Calendar
|
18
18
|
when 'events' then Event
|
19
19
|
when 'tasks' then Reminder
|
@@ -23,7 +23,11 @@ module ICalPal
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
# Load data
|
26
|
+
# Load data from a database
|
27
|
+
#
|
28
|
+
# @param db_file [String] Path to the database file
|
29
|
+
# @param q [String] The query to run
|
30
|
+
# @return [Array<Hash>] Array of rows returned by the query
|
27
31
|
def self.load_data(db_file, q)
|
28
32
|
$log.debug(q.gsub("\n", ' '))
|
29
33
|
|
@@ -40,7 +44,7 @@ module ICalPal
|
|
40
44
|
$opts[:props] = stmt.columns - $opts[:eep] if $opts[:props].any? 'all'
|
41
45
|
|
42
46
|
# Iterate the SQLite3::ResultSet once
|
43
|
-
stmt.execute.
|
47
|
+
stmt.execute.each { |i| rows.push(i) }
|
44
48
|
stmt.close
|
45
49
|
|
46
50
|
# Close the database
|
@@ -66,25 +70,30 @@ module ICalPal
|
|
66
70
|
rows
|
67
71
|
end
|
68
72
|
|
69
|
-
#
|
73
|
+
# Initialize fields common to all ICalPal classes
|
74
|
+
#
|
75
|
+
# @param obj [ICalPal] An +Store+, +Calendar+, +Event+, or +Reminder+
|
70
76
|
def initialize(obj)
|
71
|
-
|
72
|
-
type = EventKit::EKSourceType[obj['type']]
|
77
|
+
@self = obj
|
73
78
|
|
74
79
|
obj['store'] = obj['account']
|
75
80
|
|
81
|
+
obj['type'] = EventKit::EKSourceType.find_index { |i| i[:name] == 'Subscribed' } if obj['subcal_url']
|
82
|
+
return unless obj['type']
|
83
|
+
|
84
|
+
type = EventKit::EKSourceType[obj['type']]
|
85
|
+
|
76
86
|
obj['type'] = type[:name]
|
77
87
|
obj['color'] ||= type[:color]
|
78
88
|
obj['symbolic_color_name'] ||= type[:color]
|
79
|
-
|
80
|
-
@self = obj
|
81
89
|
end
|
82
90
|
|
83
91
|
# Create a new CSV::Row with values from +self+. Newlines are
|
84
92
|
# replaced with '\n' to ensure each Row is a single line of text.
|
85
93
|
#
|
86
94
|
# @param headers [Array] Key names used as the header row in a CSV::Table
|
87
|
-
# @return [CSV::Row] The +Store+, +Calendar+,
|
95
|
+
# @return [CSV::Row] The +Store+, +Calendar+, +CalendarItem+, or
|
96
|
+
# +Reminder+ as a CSV::Row
|
88
97
|
def to_csv(headers)
|
89
98
|
values = headers.map { |h| (@self[h].respond_to?(:gsub))? @self[h].gsub("\n", '\n') : @self[h] }
|
90
99
|
|
data/lib/options.rb
CHANGED
@@ -79,7 +79,7 @@ module ICalPal
|
|
79
79
|
@op.on('--el=LISTS', Array, 'List of reminder lists to exclude')
|
80
80
|
|
81
81
|
@op.separator('')
|
82
|
-
@op.on('--match=FIELD=
|
82
|
+
@op.on('--match=FIELD=REGEX', String, 'Include only items whose FIELD matches REGEX (ignoring case)')
|
83
83
|
|
84
84
|
# dates
|
85
85
|
@op.separator("\nChoosing dates:\n\n")
|
@@ -233,10 +233,15 @@ module ICalPal
|
|
233
233
|
rescue StandardError
|
234
234
|
end unless cli[:norc]
|
235
235
|
|
236
|
+
# Find command
|
236
237
|
cli[:cmd] ||= @op.default_argv[0]
|
237
238
|
cli[:cmd] ||= env[:cmd] if env[:cmd]
|
238
239
|
cli[:cmd] ||= cf[:cmd] if cf[:cmd]
|
239
|
-
|
240
|
+
|
241
|
+
# Handle command aliases
|
242
|
+
cli[:cmd] = 'accounts' if cli[:cmd] == 'stores'
|
243
|
+
cli[:cmd] = cli[:cmd].sub('reminders', 'tasks')
|
244
|
+
cli[:cmd] = cli[:cmd].sub('datedReminders', 'datedTasks')
|
240
245
|
|
241
246
|
# Parse eventsNow and eventsToday commands
|
242
247
|
cli[:cmd].match('events(Now|Today|Remaining)(\+[0-9]+)?') do |m|
|
@@ -327,7 +332,7 @@ module ICalPal
|
|
327
332
|
end
|
328
333
|
|
329
334
|
# Commands that can be run
|
330
|
-
COMMANDS = %w[events eventsToday eventsNow eventsRemaining tasks datedTasks undatedTasks calendars accounts
|
335
|
+
COMMANDS = %w[events eventsToday eventsNow eventsRemaining tasks datedTasks undatedTasks calendars accounts].freeze
|
331
336
|
|
332
337
|
# Supported output formats
|
333
338
|
OUTFORMATS = %w[ansi csv default hash html json md rdoc remind toc xml yaml].freeze
|
data/lib/reminder.rb
CHANGED
@@ -1,20 +1,59 @@
|
|
1
|
-
r '
|
2
|
-
|
1
|
+
r 'timezone'
|
2
|
+
|
3
|
+
# Images
|
4
|
+
# Section
|
3
5
|
|
4
6
|
module ICalPal
|
5
7
|
# Class representing items from the <tt>Reminders</tt> database
|
6
8
|
class Reminder
|
7
9
|
include ICalPal
|
8
10
|
|
11
|
+
def self.load_data(db_file, q)
|
12
|
+
# Load items
|
13
|
+
ICalPal.load_data(db_file, q)
|
14
|
+
end
|
15
|
+
|
9
16
|
def [](k)
|
10
17
|
case k
|
11
|
-
when '
|
12
|
-
|
18
|
+
when 'alert' # -N (minutes|hours|days|weeks|months)
|
19
|
+
if @self['alert']
|
20
|
+
alert = JSON.parse(@self['alert'])['dueDateDeltaAlerts'][0]
|
21
|
+
count = alert['dueDateDeltaCount'] * -1
|
22
|
+
unit = EventKit::EKReminderDueDateDeltaUnit[alert['dueDateDeltaUnit']]
|
23
|
+
"#{count} #{unit}"
|
24
|
+
end
|
25
|
+
|
26
|
+
when 'assignee' # [ nickname, firstname, lastname, address ]
|
27
|
+
if @self['assignee']
|
28
|
+
a = @self['assignee']
|
29
|
+
t = (a[0])? a[0] : "#{a[1]} #{a[2]}"
|
30
|
+
t += " (#{a[3][7..]})" if a[3]
|
31
|
+
t
|
32
|
+
end
|
33
|
+
|
34
|
+
when 'due' # date[ at time]
|
35
|
+
if @self['due']
|
36
|
+
t = @self['due'].to_s
|
37
|
+
t += " at #{@self['due'].strftime($opts[:tf])}" unless @self['all_day'] == 1
|
38
|
+
t
|
39
|
+
end
|
40
|
+
|
41
|
+
when 'group' # (group|"(no group)")
|
42
|
+
(@self['group'])? @self['group'] : '(no group)'
|
13
43
|
|
14
44
|
when 'priority' # Integer -> String
|
15
45
|
EventKit::EKReminderPriority[@self['priority']] if @self['priority'].positive?
|
16
46
|
|
47
|
+
when 'proximity' # (arriving|leaving)
|
48
|
+
EventKit::EKReminderProximity[@self['proximity']] if @self['proximity']
|
49
|
+
|
50
|
+
when 'radius' # Float -> Integer
|
51
|
+
"#{Integer(@self['radius'])}m" if @self['radius']
|
52
|
+
|
17
53
|
when 'sdate' # For sorting
|
54
|
+
@self['due_date']
|
55
|
+
|
56
|
+
when 'name', 'reminder', 'task' # Aliases
|
18
57
|
@self['title']
|
19
58
|
|
20
59
|
else @self[k]
|
@@ -22,8 +61,24 @@ module ICalPal
|
|
22
61
|
end
|
23
62
|
|
24
63
|
def initialize(obj)
|
25
|
-
|
26
|
-
|
64
|
+
super
|
65
|
+
|
66
|
+
# Convert JSON arrays to Arrays
|
67
|
+
@self['tags'] = JSON.parse(obj['tags']) if obj['tags']
|
68
|
+
@self['location'] = JSON.parse(obj['location']).compact.uniq[0] if obj['location']
|
69
|
+
@self['proximity'] = JSON.parse(obj['proximity']).compact.uniq[0] if obj['proximity']
|
70
|
+
@self['radius'] = JSON.parse(obj['radius']).compact.uniq[0] if obj['radius']
|
71
|
+
@self['assignee'] = JSON.parse(obj['assignee']) if obj['assignee']
|
72
|
+
|
73
|
+
# Section
|
74
|
+
if @self['members']
|
75
|
+
j = JSON.parse(@self['members']).select { |i| i['memberID'] == @self['id'] }
|
76
|
+
s = $sections.select { |i| i['id'] == j[0]['groupID'] } if j[0]
|
77
|
+
@self['section'] = s[0]['name'] if s && s[0]
|
78
|
+
|
79
|
+
@self.delete('members')
|
80
|
+
@self.delete('id')
|
81
|
+
end
|
27
82
|
|
28
83
|
# Priority
|
29
84
|
# rubocop: disable Style/NumericPredicate
|
@@ -33,14 +88,21 @@ module ICalPal
|
|
33
88
|
@self['prio'] = 3 if @self['priority'] == 0 # none
|
34
89
|
# rubocop: enable Style/NumericPredicate
|
35
90
|
|
36
|
-
@self['long_priority'] = LONG_PRIORITY[@self['prio']]
|
91
|
+
@self['long_priority'] = LONG_PRIORITY[@self['prio']] if @self['prio']
|
37
92
|
|
38
93
|
# For sorting
|
39
94
|
@self['sdate'] = (@self['title'])? @self['title'] : ''
|
40
95
|
|
41
96
|
# Due date
|
42
|
-
|
43
|
-
|
97
|
+
if @self['due_date']
|
98
|
+
begin
|
99
|
+
zone = Timezone.fetch(@self['timezone'])
|
100
|
+
rescue Timezone::Error::InvalidZone
|
101
|
+
zone = '+00:00'
|
102
|
+
end
|
103
|
+
|
104
|
+
@self['due'] = RDT.from_time(Time.at(@self['due_date'] + ITIME, in: zone))
|
105
|
+
end
|
44
106
|
|
45
107
|
# Notes
|
46
108
|
@self['notes'] = '' unless @self['notes']
|
@@ -49,22 +111,33 @@ module ICalPal
|
|
49
111
|
@self['color'] = nil unless $opts[:palette]
|
50
112
|
|
51
113
|
if @self['color']
|
52
|
-
|
53
|
-
stdin, stdout, _stderr, _e = Open3.popen3(PL_CONVERT)
|
54
|
-
|
55
|
-
# Send color bplist
|
56
|
-
stdin.write(@self['color'])
|
57
|
-
stdin.close
|
114
|
+
plist = plconvert(@self['color'])
|
58
115
|
|
59
|
-
#
|
60
|
-
plist
|
61
|
-
|
62
|
-
|
63
|
-
|
116
|
+
# Get color and symbolic color name
|
117
|
+
plist.each do |p|
|
118
|
+
@self['color'] = plist[p['daHexString']['CF$UID']] if p['daHexString']
|
119
|
+
@self['symbolic_color_name'] = plist[p['ckSymbolicColorName']['CF$UID']] if p['ckSymbolicColorName']
|
120
|
+
end
|
64
121
|
else
|
65
122
|
@self['color'] = DEFAULT_COLOR
|
66
123
|
@self['symbolic_color_name'] = DEFAULT_SYMBOLIC_COLOR
|
67
124
|
end
|
125
|
+
|
126
|
+
# Contacts
|
127
|
+
messaging = []
|
128
|
+
|
129
|
+
plist = plconvert(@self['messaging'])
|
130
|
+
plist.each do |p|
|
131
|
+
%w[ emails phones ].each do |field|
|
132
|
+
next unless p[field]
|
133
|
+
|
134
|
+
offset = p[field].values[0]
|
135
|
+
targets = plist[offset]['NS.objects']
|
136
|
+
targets.each { |t| messaging.push(plist[t['CF$UID']]) }
|
137
|
+
end
|
138
|
+
end if plist
|
139
|
+
|
140
|
+
@self['messaging'] = messaging
|
68
141
|
end
|
69
142
|
|
70
143
|
DEFAULT_COLOR = '#1BADF8'.freeze
|
@@ -77,32 +150,105 @@ module ICalPal
|
|
77
150
|
'No priority',
|
78
151
|
].freeze
|
79
152
|
|
80
|
-
PL_CONVERT = '/usr/bin/plutil -convert xml1 -o - -'.freeze
|
81
|
-
|
82
153
|
DB_PATH = "#{Dir.home}/Library/Group Containers/group.com.apple.reminders/Container_v1/Stores".freeze
|
83
154
|
|
84
155
|
QUERY = <<~SQL.freeze
|
85
156
|
SELECT DISTINCT
|
86
157
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
158
|
+
r1.zTitle as title,
|
159
|
+
r1.zAllday as all_day,
|
160
|
+
r1.zDueDate as due_date,
|
161
|
+
r1.zFlagged as flagged,
|
162
|
+
r1.zNotes as notes,
|
163
|
+
r1.zPriority as priority,
|
164
|
+
r1.zContactHandles as messaging,
|
165
|
+
r1.zDueDateDeltaAlertsData as alert,
|
166
|
+
r1.zTimezone as timezone,
|
167
|
+
r1.zckIdentifier as id,
|
168
|
+
|
169
|
+
bl1.zBadgeEmblem as badge,
|
170
|
+
bl1.zColor as color,
|
171
|
+
bl1.zName as list_name,
|
172
|
+
bl1.zParentList as parent,
|
173
|
+
bl1.zSharingStatus as shared,
|
174
|
+
|
175
|
+
-- section members
|
176
|
+
json(bl1.ZMembershipsOfRemindersInSectionsAsData) -> '$.memberships' AS members,
|
177
|
+
|
178
|
+
-- group
|
179
|
+
(SELECT zName
|
180
|
+
FROM zremcdBaseList bl2
|
181
|
+
WHERE bl2.z_pk = bl1.zParentList) AS 'group',
|
182
|
+
|
183
|
+
-- location
|
184
|
+
(SELECT json_group_array(zremcdObject.zTitle)
|
185
|
+
FROM zremcdObject
|
186
|
+
WHERE zremcdObject.z_pk IN (
|
187
|
+
SELECT zTrigger
|
188
|
+
FROM zremcdObject
|
189
|
+
WHERE zremcdObject.zReminder = r1.z_pk
|
190
|
+
)) AS location,
|
191
|
+
|
192
|
+
-- proximity
|
193
|
+
(SELECT json_group_array(zremcdObject.zProximity)
|
194
|
+
FROM zremcdObject
|
195
|
+
WHERE zremcdObject.z_pk IN (
|
196
|
+
SELECT zTrigger
|
197
|
+
FROM zremcdObject
|
198
|
+
WHERE zremcdObject.zReminder = r1.z_pk
|
199
|
+
)) AS proximity,
|
93
200
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
201
|
+
-- radius
|
202
|
+
(SELECT json_group_array(zremcdObject.zRadius)
|
203
|
+
FROM zremcdObject
|
204
|
+
WHERE zremcdObject.z_pk IN (
|
205
|
+
SELECT zTrigger
|
206
|
+
FROM zremcdObject
|
207
|
+
WHERE zremcdObject.zReminder = r1.z_pk
|
208
|
+
)) AS radius,
|
99
209
|
|
100
|
-
|
210
|
+
-- tags
|
211
|
+
(SELECT json_group_array(zName)
|
212
|
+
FROM zremcdHashtagLabel
|
213
|
+
WHERE zremcdHashtagLabel.z_pk IN (
|
214
|
+
SELECT zremcdObject.zHashtagLabel
|
215
|
+
FROM zremcdObject
|
216
|
+
JOIN zremcdreminder ON zremcdObject.zReminder3 = r1.z_pk
|
217
|
+
WHERE zremcdObject.zReminder3 = r1.z_pk
|
218
|
+
)) AS tags,
|
219
|
+
|
220
|
+
-- assignee
|
221
|
+
(SELECT
|
222
|
+
json_array(zNickname, zFirstName, zLastName, zAddress1)
|
223
|
+
FROM zremcdObject
|
224
|
+
WHERE z_pk = (
|
225
|
+
SELECT zAssignee
|
226
|
+
FROM zremcdObject
|
227
|
+
WHERE zReminder1 = r1.z_pk
|
228
|
+
)) AS assignee,
|
229
|
+
|
230
|
+
-- url
|
231
|
+
(SELECT zURL
|
232
|
+
FROM zremcdObject
|
233
|
+
WHERE zReminder2 = r1.z_pk) AS url
|
234
|
+
|
235
|
+
FROM zremcdReminder r1
|
236
|
+
|
237
|
+
LEFT OUTER JOIN zremcdBaseList bl1 ON r1.zList = bl1.z_pk
|
238
|
+
|
239
|
+
WHERE r1.zCompleted = 0
|
240
|
+
AND r1.zMarkedForDeletion = 0
|
241
|
+
|
242
|
+
SQL
|
243
|
+
|
244
|
+
# Load sections
|
245
|
+
SECTIONS_QUERY = <<~SQL.freeze
|
246
|
+
SELECT DISTINCT
|
101
247
|
|
102
|
-
|
248
|
+
zckIdentifier AS id,
|
249
|
+
zDisplayName AS name
|
103
250
|
|
104
|
-
|
105
|
-
AND zremcdReminder.zMarkedForDeletion = 0
|
251
|
+
FROM zremcdBaseSection
|
106
252
|
|
107
253
|
SQL
|
108
254
|
|
data/lib/store.rb
CHANGED
@@ -3,16 +3,42 @@ module ICalPal
|
|
3
3
|
class Store
|
4
4
|
include ICalPal
|
5
5
|
|
6
|
+
def [](k)
|
7
|
+
case k
|
8
|
+
when 'name', 'title' # Aliases
|
9
|
+
@self['account']
|
10
|
+
|
11
|
+
when 'owner' # Owner iff there is an account
|
12
|
+
(@self['owner'] == @self['account'])? nil : @self['owner']
|
13
|
+
|
14
|
+
else @self[k]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(obj)
|
19
|
+
super
|
20
|
+
|
21
|
+
# Convert JSON arrays to Arrays
|
22
|
+
@self['delegations'] = JSON.parse(obj['delegations']).sort if obj['delegations']
|
23
|
+
end
|
24
|
+
|
6
25
|
QUERY = <<~SQL.freeze
|
7
26
|
SELECT DISTINCT
|
8
27
|
|
9
|
-
|
10
|
-
|
28
|
+
s1.name AS account,
|
29
|
+
s1.owner_name AS owner,
|
30
|
+
s1.notes,
|
31
|
+
s1.type,
|
32
|
+
|
33
|
+
(SELECT json_group_array(name)
|
34
|
+
FROM #{self.name.split('::').last} s2
|
35
|
+
WHERE s2.delegated_account_owner_store_id == s1.external_id
|
36
|
+
) AS delegations
|
37
|
+
|
38
|
+
FROM #{self.name.split('::').last} s1
|
11
39
|
|
12
|
-
|
40
|
+
WHERE s1.delegated_account_owner_store_id IS NULL
|
13
41
|
|
14
|
-
WHERE Store.disabled IS NOT 1
|
15
|
-
AND Store.display_order IS NOT -1
|
16
42
|
SQL
|
17
43
|
|
18
44
|
end
|
data/lib/utils.rb
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
PL_CONVERT = '/usr/bin/plutil -convert xml1 -o - -'.freeze
|
2
|
+
|
3
|
+
# Load a plist
|
4
|
+
#
|
5
|
+
# @param obj [String] Data that can be converted by +/usr/bin/plutil+
|
6
|
+
# @return [Array] Objects representing nodes in the plist
|
7
|
+
def plconvert(obj)
|
8
|
+
r 'open3'
|
9
|
+
r 'plist'
|
10
|
+
|
11
|
+
# Run PL_CONVERT command
|
12
|
+
sin, sout, _serr, _e = Open3.popen3(PL_CONVERT)
|
13
|
+
|
14
|
+
# Send obj
|
15
|
+
sin.write(obj)
|
16
|
+
sin.close
|
17
|
+
|
18
|
+
# Read output
|
19
|
+
begin
|
20
|
+
plist = Plist.parse_xml(sout.read)
|
21
|
+
plist['$objects'] if plist
|
22
|
+
rescue Plist::UnimplementedElementError
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
1
27
|
# Convert a key/value pair to XML. The value should be +nil+, +String+,
|
2
28
|
# +Integer+, +Array+, or +ICalPal::RDT+
|
3
29
|
#
|
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.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Rosen
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-08-06 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
12
|
description: |
|
13
13
|
Inspired by icalBuddy and maintains close compatability. Includes
|