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