icalPal 2.1.0 → 3.0.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 +24 -20
- data/bin/icalPal +23 -9
- data/bin/icalpal +320 -0
- data/icalPal.gemspec +2 -1
- data/lib/EventKit.rb +1 -0
- data/lib/defaults.rb +2 -1
- data/lib/event.rb +97 -66
- data/lib/icalPal.rb +2 -2
- data/lib/options.rb +5 -2
- data/lib/version.rb +2 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 25a711f9e4887a81dafccbc57e0a2fa84530dc8cee81e58f0d5df6041783bec2
|
4
|
+
data.tar.gz: 698a498065687f7a7d06e6c316d1937bfc3b1a58d7a661b612bb8d39b5429318
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9cf18231c95a94b663e1a41ecd16326f447fb038864677ad05ba2a6fa5f684ae999732401ff30b9df1b0739ff31f78f7b0385e3f7774d5260b9cfbc37a6d2680
|
7
|
+
data.tar.gz: d3aaf37df004da104518e80463c2bc6933ba87277fc9c913bb73a4653055a93417694fb1de655d1c3776f3d47d45afaf241b5ba59361894bafa7ebd1bf253c29
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
[](https://badge.fury.io/rb/icalpal)
|
2
2
|
|
3
|
-
#
|
3
|
+
# icalpal
|
4
4
|
|
5
5
|
## Description
|
6
6
|
|
7
|
-
|
7
|
+
icalpal is a command-line tool to query a 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.
|
@@ -14,27 +14,27 @@ Calendar or Reminders database.
|
|
14
14
|
As a system-wide Ruby gem:
|
15
15
|
|
16
16
|
```
|
17
|
-
gem install
|
17
|
+
gem install icalpal
|
18
18
|
```
|
19
19
|
|
20
20
|
or in your home diretory:
|
21
21
|
|
22
22
|
```
|
23
|
-
gem install --user-install
|
23
|
+
gem install --user-install icalpal
|
24
24
|
```
|
25
25
|
|
26
26
|
As a Homebrew formula:
|
27
27
|
|
28
28
|
```
|
29
|
-
brew tap ajrosen/
|
30
|
-
brew install
|
29
|
+
brew tap ajrosen/icalpal
|
30
|
+
brew install icalpal
|
31
31
|
```
|
32
32
|
|
33
33
|
## Features
|
34
34
|
|
35
35
|
### Compatability with [icalBuddy](https://github.com/ali-rantakari/icalBuddy)
|
36
36
|
|
37
|
-
|
37
|
+
icalpal tries to be compatible with icalBuddy for command-line options
|
38
38
|
and for output. There are a some important differences to be aware
|
39
39
|
of.
|
40
40
|
|
@@ -48,17 +48,17 @@ of.
|
|
48
48
|
|
49
49
|
### Additional commands
|
50
50
|
|
51
|
-
```
|
51
|
+
```icalpal accounts```
|
52
52
|
|
53
|
-
Shows a list of enabled Calendar accounts. Internally they are known as *Stores*; you can run ```
|
53
|
+
Shows a list of enabled Calendar accounts. Internally they are known as *Stores*; you can run ```icalpal stores``` instead.
|
54
54
|
|
55
|
-
```
|
55
|
+
```icalpal datedTasks```
|
56
56
|
|
57
57
|
Shows only reminders that have a due date.
|
58
58
|
|
59
59
|
### Additional options
|
60
60
|
|
61
|
-
* Options can be abbreviated, so long as they are unique. Eg., ```
|
61
|
+
* 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```.
|
62
62
|
* The ```-c``` part is optional, but you cannot abbreviate the command if you leave it off.
|
63
63
|
* Use ```-o``` to print the output in different formats. CSV or JSON are intertesting choices.
|
64
64
|
* Copy your Calendar database file and use ```--db``` on it.
|
@@ -68,12 +68,13 @@ Shows only reminders that have a due date.
|
|
68
68
|
* ```--aep``` is like ```--iep```, but *adds* to the default property list instead of replacing it.
|
69
69
|
* ```--sep``` to separate by any property, not just calendar (```--sc```) or date (```--sd```)
|
70
70
|
* ```--color``` uses a wider color palette. Calendar colors are what you have chosen in the Calendar app. Not supported in all terminals, but looks great in [iTerm2](https://iterm2.com/).
|
71
|
+
* ```--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
|
71
72
|
|
72
|
-
Because
|
73
|
+
Because icalpal is written in Ruby, and not a native Mac application, you can run it just about anywhere. It's been tested with the version of Ruby (2.6.10) included with macOS.
|
73
74
|
|
74
75
|
## Usage
|
75
76
|
|
76
|
-
|
77
|
+
icalpal: Usage: icalpal [options] [-c] COMMAND
|
77
78
|
|
78
79
|
COMMAND must be one of the following:
|
79
80
|
```
|
@@ -95,7 +96,7 @@ Global options:
|
|
95
96
|
--db=DB Use DB file instead of Calendar (default: /Users/ajr/Library/Calendars/Calendar.sqlitedb)
|
96
97
|
For the tasks commands this should be a directory containing .sqlite files
|
97
98
|
(default: /Users/ajr/Library/Group Containers/group.com.apple.reminders/Container_v1/Stores)
|
98
|
-
--cf=FILE Set config file path (default: /Users/ajr/.
|
99
|
+
--cf=FILE Set config file path (default: /Users/ajr/.icalpal)
|
99
100
|
-o, --output=FORMAT Print as FORMAT (default: default)
|
100
101
|
[ansi, csv, default, hash, html, json, md, rdoc, remind, toc, xml, yaml]
|
101
102
|
```
|
@@ -114,6 +115,9 @@ Including/excluding calendars and reminders:
|
|
114
115
|
|
115
116
|
--il=LISTS List of reminder lists to include
|
116
117
|
--el=LISTS List of reminder lists to exclude
|
118
|
+
|
119
|
+
--match=FIELD=REGEXP
|
120
|
+
Include only items whose FIELD matches REGEXP (ignoring case)
|
117
121
|
```
|
118
122
|
|
119
123
|
Choosing dates:
|
@@ -196,19 +200,19 @@ Environment variables:
|
|
196
200
|
```
|
197
201
|
ICALPAL Additional arguments
|
198
202
|
ICALPAL_CONFIG Additional arguments from a file
|
199
|
-
(default: /Users/ajr/.
|
203
|
+
(default: /Users/ajr/.icalpal)
|
200
204
|
```
|
201
205
|
|
202
206
|
## Output formats
|
203
207
|
|
204
|
-
|
208
|
+
icalpal supports several output formats. The **default** format tries
|
205
209
|
to mimic icalBuddy as much as possible.
|
206
210
|
|
207
211
|
CSV, Hash, JSON, XML, and YAML print all fields for all items in their
|
208
212
|
respective formats. From that you can analyze the results any way you
|
209
213
|
like.
|
210
214
|
|
211
|
-
[Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built in
|
215
|
+
[Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built in icalpal.
|
212
216
|
|
213
217
|
Other formats such as ANSI, HTML, Markdown, RDoc, and TOC, use Ruby's
|
214
218
|
[RDoc::Markup](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup.html)
|
@@ -256,9 +260,9 @@ Lawton](https://github.com/jimlawton) that it even compiles anymore.
|
|
256
260
|
Instead of trying to understand and extend the existing code, I chose
|
257
261
|
to start anew using my language of choice: Ruby. Using Ruby meant
|
258
262
|
there is *much* less code; about 1,600 lines vs. 7,000. It also means
|
259
|
-
|
263
|
+
icalpal is multi-platform.
|
260
264
|
|
261
265
|
I won't pretend to understand **why** you would want to run this on
|
262
|
-
Linux or Windows. But since
|
266
|
+
Linux or Windows. But since icalpal is written in Ruby and gets its
|
263
267
|
data directly from the Calendar and Reminders database files instead
|
264
268
|
of an API, you *can*.
|
data/bin/icalPal
CHANGED
@@ -9,12 +9,12 @@ begin
|
|
9
9
|
require 'sqlite3'
|
10
10
|
require 'yaml'
|
11
11
|
|
12
|
-
require_relative '../lib/
|
12
|
+
require_relative '../lib/icalpal'
|
13
13
|
require_relative '../lib/options'
|
14
14
|
rescue LoadError => e
|
15
15
|
dep = e.message[/-- (.*)/, 1]
|
16
16
|
|
17
|
-
$stderr.puts "FATAL:
|
17
|
+
$stderr.puts "FATAL: icalpal is missing a dependency: #{dep}"
|
18
18
|
$stderr.puts
|
19
19
|
$stderr.puts "Install with 'gem install --user-install #{dep}'"
|
20
20
|
|
@@ -95,14 +95,16 @@ $rows.each_with_index do |row, i|
|
|
95
95
|
end
|
96
96
|
|
97
97
|
# --ec/--ic
|
98
|
-
|
99
|
-
$
|
100
|
-
|
101
|
-
|
98
|
+
unless klass == ICalPal::Store or !row['calendar']
|
99
|
+
if $opts[:ec].any? row['calendar'] then
|
100
|
+
$log.debug(":ec")
|
101
|
+
next
|
102
|
+
end
|
102
103
|
|
103
|
-
|
104
|
-
|
105
|
-
|
104
|
+
unless $opts[:ic].empty? or $opts[:ic].any? row['calendar']
|
105
|
+
$log.debug(":ic")
|
106
|
+
next
|
107
|
+
end
|
106
108
|
end
|
107
109
|
|
108
110
|
# Instantiate an item
|
@@ -130,6 +132,18 @@ $rows.each_with_index do |row, i|
|
|
130
132
|
next
|
131
133
|
end
|
132
134
|
|
135
|
+
# --match
|
136
|
+
if $opts[:match]
|
137
|
+
r = $opts[:match].split('=')
|
138
|
+
|
139
|
+
if item[r[0]].to_s.respond_to?(:match)
|
140
|
+
unless item[r[0]].to_s.match(Regexp.new(r[1].to_s, Regexp::IGNORECASE)) then
|
141
|
+
$log.debug(":match")
|
142
|
+
next
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
133
147
|
if ICalPal::Event === item
|
134
148
|
# Check for all-day and cancelled events
|
135
149
|
if $opts[:ea] && item['all_day'].positive? then
|
data/bin/icalpal
ADDED
@@ -0,0 +1,320 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
require 'csv'
|
7
|
+
require 'json'
|
8
|
+
require 'rdoc'
|
9
|
+
require 'sqlite3'
|
10
|
+
require 'yaml'
|
11
|
+
|
12
|
+
require_relative '../lib/icalpal'
|
13
|
+
require_relative '../lib/options'
|
14
|
+
rescue LoadError => e
|
15
|
+
dep = e.message[/-- (.*)/, 1]
|
16
|
+
|
17
|
+
$stderr.puts "FATAL: icalpal is missing a dependency: #{dep}"
|
18
|
+
$stderr.puts
|
19
|
+
$stderr.puts "Install with 'gem install --user-install #{dep}'"
|
20
|
+
|
21
|
+
exit
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
##################################################
|
26
|
+
# Load options
|
27
|
+
|
28
|
+
# All kids love log!
|
29
|
+
$log = Logger.new(STDERR, { level: $defaults[:common][:debug] })
|
30
|
+
$log.formatter = proc do |s, t, p, m| # Severity, time, progname, msg
|
31
|
+
($log.level.positive?)? "#{s}: #{m}\n" :
|
32
|
+
"[%-5s] %s [%s] - %s\n" %
|
33
|
+
[ s, t.strftime('%H:%M:%S.%L'), caller(4, 1)[0].split('/')[-1], m ]
|
34
|
+
end
|
35
|
+
|
36
|
+
$opts = ICalPal::Options.new.parse_options
|
37
|
+
|
38
|
+
$rows = [] # Rows from the database
|
39
|
+
$items = [] # Items to be printed
|
40
|
+
|
41
|
+
|
42
|
+
##################################################
|
43
|
+
# All kids love log!
|
44
|
+
|
45
|
+
$log.info("Options: #{$opts}")
|
46
|
+
|
47
|
+
|
48
|
+
##################################################
|
49
|
+
# Add an item to the list
|
50
|
+
#
|
51
|
+
# @param item[Object]
|
52
|
+
|
53
|
+
def add(item)
|
54
|
+
$log.info("Adding #{item.inspect} #{item['UUID']} (#{item['title']})") if item['UUID']
|
55
|
+
|
56
|
+
item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if item === ICalPal::Event && item['sdate']
|
57
|
+
$items.push(item)
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
##################################################
|
62
|
+
# Load the data
|
63
|
+
|
64
|
+
# What are we getting?
|
65
|
+
klass = ICalPal::($opts[:cmd])
|
66
|
+
|
67
|
+
# Get it
|
68
|
+
if klass == ICalPal::Reminder then
|
69
|
+
# Load all .sqlite files
|
70
|
+
Dir.glob("#{$opts[:db]}/*.sqlite").each { |db| $rows += ICalPal.load_data(db, klass::QUERY) }
|
71
|
+
else
|
72
|
+
# Load database
|
73
|
+
$rows += ICalPal.load_data($opts[:db], klass::QUERY)
|
74
|
+
end
|
75
|
+
|
76
|
+
$log.info("Loaded #{$rows.count} #{klass} rows")
|
77
|
+
|
78
|
+
|
79
|
+
##################################################
|
80
|
+
# Process the data
|
81
|
+
|
82
|
+
# Add rows
|
83
|
+
$rows.each_with_index do |row, i|
|
84
|
+
$log.debug("Row #{i}: #{row['ROWID']}:#{row['UUID']} - #{row['account']}/#{row['calendar']}/#{row['title']}")
|
85
|
+
|
86
|
+
# --es/--is
|
87
|
+
if $opts[:es].any? row['account'] then
|
88
|
+
$log.debug(":es")
|
89
|
+
next
|
90
|
+
end
|
91
|
+
|
92
|
+
unless $opts[:is].empty? or $opts[:is].any? row['account']
|
93
|
+
$log.debug(":is");
|
94
|
+
next
|
95
|
+
end
|
96
|
+
|
97
|
+
# --ec/--ic
|
98
|
+
unless klass == ICalPal::Store or !row['calendar']
|
99
|
+
if $opts[:ec].any? row['calendar'] then
|
100
|
+
$log.debug(":ec")
|
101
|
+
next
|
102
|
+
end
|
103
|
+
|
104
|
+
unless $opts[:ic].empty? or $opts[:ic].any? row['calendar']
|
105
|
+
$log.debug(":ic")
|
106
|
+
next
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Instantiate an item
|
111
|
+
item = klass.new(row)
|
112
|
+
|
113
|
+
# --et/--it
|
114
|
+
if $opts[:et].any? item['type'] then
|
115
|
+
$log.debug(":et")
|
116
|
+
next
|
117
|
+
end
|
118
|
+
|
119
|
+
unless $opts[:it].empty? or $opts[:it].any? item['type']
|
120
|
+
$log.debug(":it")
|
121
|
+
next
|
122
|
+
end
|
123
|
+
|
124
|
+
# --el/--il
|
125
|
+
if $opts[:el].any? item['list_name'] then
|
126
|
+
$log.debug(":el")
|
127
|
+
next
|
128
|
+
end
|
129
|
+
|
130
|
+
unless $opts[:il].empty? or $opts[:il].any? item['list_name']
|
131
|
+
$log.debug(":il")
|
132
|
+
next
|
133
|
+
end
|
134
|
+
|
135
|
+
# --match
|
136
|
+
if $opts[:match]
|
137
|
+
r = $opts[:match].split('=')
|
138
|
+
|
139
|
+
if item[r[0]].to_s.respond_to?(:match)
|
140
|
+
unless item[r[0]].to_s.match(Regexp.new(r[1].to_s, Regexp::IGNORECASE)) then
|
141
|
+
$log.debug(":match")
|
142
|
+
next
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
if ICalPal::Event === item
|
148
|
+
# Check for all-day and cancelled events
|
149
|
+
if $opts[:ea] && item['all_day'].positive? then
|
150
|
+
$log.debug(":ea")
|
151
|
+
next
|
152
|
+
end
|
153
|
+
|
154
|
+
if $opts[:ia] && !item['all_day'].positive? then
|
155
|
+
$log.debug(":ia")
|
156
|
+
next
|
157
|
+
end
|
158
|
+
|
159
|
+
if item['status'] == :canceled then
|
160
|
+
$log.debug(":canceled")
|
161
|
+
next
|
162
|
+
end
|
163
|
+
|
164
|
+
(item['has_recurrences'].positive?)?
|
165
|
+
item.recurring.each { |i| add(i) } :
|
166
|
+
item.non_recurring.each { |i| add(i) }
|
167
|
+
else
|
168
|
+
# Check for dated reminders
|
169
|
+
if ICalPal::Reminder === item then
|
170
|
+
if $opts[:dated] == 1 and item['due_date'] > 0 then
|
171
|
+
$log.debug(":undated")
|
172
|
+
next
|
173
|
+
end
|
174
|
+
|
175
|
+
if $opts[:dated] == 2 and item['due_date'] == 0 then
|
176
|
+
$log.debug(":dated")
|
177
|
+
next
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
add(item)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Add placeholders for empty days
|
186
|
+
if $opts[:sed] && $opts[:sd] && klass == ICalPal::Event
|
187
|
+
days = $items.collect { |i| i['sday'] }.uniq.sort
|
188
|
+
|
189
|
+
$opts[:days].times do |n|
|
190
|
+
day = $opts[:from] + n
|
191
|
+
$items.push(klass.new(day)) unless days.any? { |i| i.to_s == day.to_s }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Sort the rows
|
196
|
+
begin
|
197
|
+
$log.info("Sorting/uniqing #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
|
198
|
+
|
199
|
+
$items.sort_by! { |i| [ i[$opts[:sep]], i[$opts[:sort]], i['sdate'] ] }
|
200
|
+
$items.reverse! if $opts[:reverse]
|
201
|
+
$items.uniq!
|
202
|
+
rescue Exception => e
|
203
|
+
$log.info("Sorting failed: #{e}\n")
|
204
|
+
end
|
205
|
+
|
206
|
+
$log.debug("#{$items.count} items remain")
|
207
|
+
|
208
|
+
# Configure formatting
|
209
|
+
mu = case $opts[:output]
|
210
|
+
when 'ansi' then RDoc::Markup::ToAnsi.new
|
211
|
+
when 'default' then RDoc::Markup::ToICalPal.new($opts)
|
212
|
+
when 'html' then
|
213
|
+
rdoc = RDoc::Options.new
|
214
|
+
rdoc.pipe = true
|
215
|
+
rdoc.output_decoration = false
|
216
|
+
RDoc::Markup::ToHtml.new(rdoc)
|
217
|
+
when 'md' then RDoc::Markup::ToMarkdown.new
|
218
|
+
when 'rdoc' then RDoc::Markup::ToRdoc.new
|
219
|
+
when 'toc' then RDoc::Markup::ToTableOfContents.new
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
##################################################
|
224
|
+
# Print the data
|
225
|
+
|
226
|
+
items = $items[0..$opts[:li] - 1]
|
227
|
+
|
228
|
+
unless mu
|
229
|
+
$log.debug("Output in #{$opts[:output]} format")
|
230
|
+
|
231
|
+
puts case $opts[:output]
|
232
|
+
when 'csv' then
|
233
|
+
# Get all headers
|
234
|
+
headers = []
|
235
|
+
items.each { |i| headers += i.keys }
|
236
|
+
headers.uniq!
|
237
|
+
|
238
|
+
# Populate a CSV::Table
|
239
|
+
table = CSV::Table.new([], headers: headers)
|
240
|
+
items.each { |i| table << i.to_csv(headers) }
|
241
|
+
|
242
|
+
table
|
243
|
+
when 'hash' then items.map { |i| i.self }
|
244
|
+
when 'json' then items.map { |i| i.self }.to_json
|
245
|
+
when 'xml' then
|
246
|
+
xml = items.map { |i| "<#{$opts[:cmd].chomp("s")}>#{i.to_xml}</#{$opts[:cmd].chomp("s")}>" }
|
247
|
+
"<#{$opts[:cmd]}>\n#{xml.join("")}</#{$opts[:cmd]}>"
|
248
|
+
when 'yaml' then items.map { |i| i.self }.to_yaml
|
249
|
+
when 'remind' then items.map { |i|
|
250
|
+
"REM #{i['sdate'].strftime('%F AT %R')} " +
|
251
|
+
"DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i } " +
|
252
|
+
"MSG #{i['title']}"
|
253
|
+
}.join("\n")
|
254
|
+
else abort "No formatter for #{$opts[:output]}"
|
255
|
+
end
|
256
|
+
|
257
|
+
exit
|
258
|
+
end
|
259
|
+
|
260
|
+
$log.debug("Formatting with #{mu.inspect}")
|
261
|
+
|
262
|
+
doc = RDoc::Markup::Document.new
|
263
|
+
section = nil
|
264
|
+
|
265
|
+
items.each_with_index do |i, j|
|
266
|
+
$log.debug("Print #{j}: #{i.inspect}")
|
267
|
+
|
268
|
+
# --li
|
269
|
+
break if $opts[:li].positive? && j >= $opts[:li]
|
270
|
+
|
271
|
+
# Use RDoc::Markup::Verbatim to save the item
|
272
|
+
v = RDoc::Markup::Verbatim.new
|
273
|
+
v.format = i
|
274
|
+
doc << v
|
275
|
+
|
276
|
+
# Sections
|
277
|
+
if $opts[:sep] && section != i[$opts[:sep]]
|
278
|
+
$log.debug("New section '#{$opts[:sep]}': #{i[$opts[:sep]]}")
|
279
|
+
|
280
|
+
doc << RDoc::Markup::Raw.new($opts[:sep])
|
281
|
+
|
282
|
+
doc << RDoc::Markup::BlankLine.new if j.positive?
|
283
|
+
doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
|
284
|
+
doc << RDoc::Markup::Rule.new(0)
|
285
|
+
|
286
|
+
section = i[$opts[:sep]]
|
287
|
+
end
|
288
|
+
|
289
|
+
# Item
|
290
|
+
props = RDoc::Markup::List.new(:BULLET)
|
291
|
+
|
292
|
+
# Properties
|
293
|
+
$opts[:props].each_with_index do |prop, k|
|
294
|
+
value = i[prop]
|
295
|
+
|
296
|
+
next unless value
|
297
|
+
next if Array === value && !value[0]
|
298
|
+
next if String === value && value.length == 0
|
299
|
+
|
300
|
+
$log.debug("#{prop}: #{value}")
|
301
|
+
|
302
|
+
# Use Raw to save the property
|
303
|
+
props << RDoc::Markup::Raw.new(prop)
|
304
|
+
|
305
|
+
unless k.positive?
|
306
|
+
# First property, value only
|
307
|
+
props << RDoc::Markup::Heading.new(2, value.to_s)
|
308
|
+
else
|
309
|
+
props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
|
310
|
+
props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless(i['placeholder'])
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Print it
|
315
|
+
props << RDoc::Markup::BlankLine.new unless props.empty?
|
316
|
+
|
317
|
+
doc << props
|
318
|
+
end
|
319
|
+
|
320
|
+
print doc.accept(mu)
|
data/icalPal.gemspec
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
require './lib/version'
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
|
-
s.name =
|
4
|
+
s.name = ICalPal::NAME
|
5
5
|
s.version = ICalPal::VERSION
|
6
|
+
|
6
7
|
s.summary = "Command-line tool to query the macOS Calendar"
|
7
8
|
s.description = <<-EOF
|
8
9
|
Inspired by icalBuddy and maintains close compatability. Includes
|
data/lib/EventKit.rb
CHANGED
data/lib/defaults.rb
CHANGED
@@ -8,7 +8,7 @@ $defaults = {
|
|
8
8
|
ab: '!',
|
9
9
|
aep: [],
|
10
10
|
bullet: '•',
|
11
|
-
cf: "#{ENV['HOME']}/.
|
11
|
+
cf: "#{ENV['HOME']}/.icalpal",
|
12
12
|
color: false,
|
13
13
|
db: "#{ENV['HOME']}/Library/Calendars/Calendar.sqlitedb",
|
14
14
|
debug: Logger::WARN,
|
@@ -26,6 +26,7 @@ $defaults = {
|
|
26
26
|
output: 'default',
|
27
27
|
ps: [ "\n " ],
|
28
28
|
r: false,
|
29
|
+
match: nil,
|
29
30
|
sc: false,
|
30
31
|
sd: false,
|
31
32
|
sep: false,
|
data/lib/event.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
1
3
|
module ICalPal
|
2
4
|
# Class representing items from the <tt>CalendarItem</tt> table
|
3
5
|
class Event
|
@@ -92,8 +94,10 @@ module ICalPal
|
|
92
94
|
end
|
93
95
|
|
94
96
|
if @self['start_tz'] == '_float'
|
95
|
-
|
96
|
-
|
97
|
+
tzoffset = Time.zone_offset($now.zone())
|
98
|
+
|
99
|
+
@self['sdate'] = RDT.new(*(@self['sdate'].to_time - tzoffset).to_a.reverse[4..], $now.zone)
|
100
|
+
@self['edate'] = RDT.new(*(@self['edate'].to_time - tzoffset).to_a.reverse[4..], $now.zone)
|
97
101
|
end
|
98
102
|
|
99
103
|
# Type of calendar event is from
|
@@ -110,20 +114,27 @@ module ICalPal
|
|
110
114
|
# If an event spans multiple days, the return value will contain
|
111
115
|
# a unique {Event} for each day that falls within our window
|
112
116
|
def non_recurring
|
113
|
-
|
117
|
+
events = []
|
118
|
+
|
119
|
+
nDays = (self['duration'] / 86400).to_i
|
120
|
+
|
121
|
+
# Sanity checks
|
122
|
+
return events if nDays > 100000
|
114
123
|
|
115
124
|
# Repeat for multi-day events
|
116
|
-
(
|
125
|
+
(nDays + 1).times do |i|
|
117
126
|
break if self['sdate'] > $opts[:to]
|
118
127
|
|
119
128
|
$log.debug("multi-day event #{i + 1}") if (i > 0)
|
129
|
+
|
120
130
|
self['daynum'] = i + 1
|
121
|
-
|
131
|
+
events.push(clone) if in_window?(self['sdate'])
|
132
|
+
|
122
133
|
self['sdate'] += 1
|
123
134
|
self['edate'] += 1
|
124
135
|
end
|
125
136
|
|
126
|
-
|
137
|
+
events
|
127
138
|
end
|
128
139
|
|
129
140
|
# Check recurring events
|
@@ -131,35 +142,49 @@ module ICalPal
|
|
131
142
|
# @return [Array<Event>]
|
132
143
|
# All occurrences of a recurring event that are within our window
|
133
144
|
def recurring
|
134
|
-
|
145
|
+
stop = [ $opts[:to], (self['rdate'] || $opts[:to]) ].min
|
135
146
|
|
136
147
|
# See if event ends before we start
|
137
|
-
stop = [ $opts[:to], (self['rdate'] || $opts[:to]) ].min
|
138
148
|
if stop < $opts[:from] then
|
139
149
|
$log.debug("#{stop} < #{$opts[:from]}")
|
140
|
-
return(
|
150
|
+
return(Array.new)
|
141
151
|
end
|
142
152
|
|
143
153
|
# Get changes to series
|
144
|
-
changes =
|
154
|
+
changes = [ { 'orig_date' => -1 } ]
|
155
|
+
changes += $rows.select { |r| r['orig_item_id'] == self['ROWID'] }
|
156
|
+
|
157
|
+
events = []
|
158
|
+
count = 1
|
145
159
|
|
146
|
-
i = 1
|
147
160
|
while self['sdate'] <= stop
|
148
|
-
|
149
|
-
|
150
|
-
|
161
|
+
# count
|
162
|
+
break if self['count'].positive? and count > self['count']
|
163
|
+
count += 1
|
164
|
+
|
165
|
+
# Handle specifier or clone self
|
166
|
+
if self['specifier'] and self['specifier'].length.positive?
|
167
|
+
occurrences = get_occurrences(changes)
|
168
|
+
else
|
169
|
+
occurrences = [ clone ]
|
151
170
|
end
|
152
|
-
i += 1
|
153
171
|
|
154
|
-
|
155
|
-
|
156
|
-
|
172
|
+
# Check for changes
|
173
|
+
occurrences.each do |occurrence|
|
174
|
+
changes.each do |change|
|
175
|
+
next if change['orig_date'] == self['sdate'].to_i - ITIME
|
176
|
+
events.push(occurrence) if in_window?(occurrence['sdate'], occurrence['edate'])
|
177
|
+
end
|
157
178
|
end
|
158
179
|
|
180
|
+
break if self['specifier']
|
159
181
|
apply_frequency!
|
160
182
|
end
|
161
183
|
|
162
|
-
|
184
|
+
# Remove exceptions
|
185
|
+
events.delete_if { |event| event['xdate'].any?(event['sdate']) }
|
186
|
+
|
187
|
+
return(events)
|
163
188
|
end
|
164
189
|
|
165
190
|
private
|
@@ -168,67 +193,72 @@ module ICalPal
|
|
168
193
|
|
169
194
|
# @return a deep clone of self
|
170
195
|
def clone()
|
171
|
-
self['stime'] = @self['sdate'].to_i
|
172
|
-
self['etime'] = @self['edate'].to_i
|
173
|
-
|
174
196
|
Marshal.load(Marshal.dump(self))
|
175
197
|
end
|
176
198
|
|
177
|
-
# Get next occurences of a recurring event
|
199
|
+
# Get next occurences of a recurring event from a specifier
|
178
200
|
#
|
179
201
|
# @param changes [Array] Recurrence changes for the event
|
180
202
|
# @return [Array<IcalPal::Event>]
|
181
203
|
def get_occurrences(changes)
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
204
|
+
occurrences = []
|
205
|
+
|
206
|
+
dow = DOW.keys
|
207
|
+
dom = [ nil ]
|
208
|
+
moy = 1..12
|
209
|
+
nth = nil
|
210
|
+
|
211
|
+
specifier = self['specifier']? self['specifier'] : []
|
212
|
+
|
213
|
+
# Deconstruct specifier
|
214
|
+
specifier.split(';').each do |k|
|
215
|
+
j = k.split('=')
|
216
|
+
|
217
|
+
# D=Day of the week, M=Day of the month, O=Month of the year, S=Nth
|
218
|
+
case j[0]
|
219
|
+
when 'D' then dow = j[1].split(',')
|
220
|
+
when 'M' then dom = j[1].split(',')
|
221
|
+
when 'O' then moy = j[1].split(',')
|
222
|
+
when 'S' then nth = j[1].to_i
|
223
|
+
else $log.warn("Unknown specifier: #{k}")
|
200
224
|
end
|
225
|
+
end
|
201
226
|
|
202
|
-
|
203
|
-
|
204
|
-
|
227
|
+
# Build array of DOWs
|
228
|
+
dows = [ nil ]
|
229
|
+
dow.each { |d| dows.push(DOW[d[-2..-1].to_sym]) }
|
205
230
|
|
206
|
-
|
207
|
-
|
208
|
-
|
231
|
+
# Months of the year (O)
|
232
|
+
moy.each do |m|
|
233
|
+
next unless m
|
209
234
|
|
210
|
-
|
211
|
-
|
212
|
-
dow = DOW[n[-2..-1].to_sym]
|
213
|
-
ndate += 1 until ndate.wday == dow
|
214
|
-
ndate = ICalPal.nth(Integer(n[0..1]), n[-2..-1], ndate) unless (n[0] == '0')
|
235
|
+
nsdate = RDT.new(self['sdate'].year, m.to_i, 1)
|
236
|
+
nedate = RDT.new(self['edate'].year, m.to_i, 1)
|
215
237
|
|
216
|
-
#
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
238
|
+
# Days of the month (M)
|
239
|
+
dom.each do |x|
|
240
|
+
next unless x
|
241
|
+
|
242
|
+
self['sdate'] = RDT.new(nsdate.year, nsdate.month, x.to_i)
|
243
|
+
self['edate'] = RDT.new(nedate.year, nedate.month, x.to_i)
|
244
|
+
occurrences.push(clone)
|
245
|
+
end
|
224
246
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
247
|
+
# Days of the week (D)
|
248
|
+
if nth
|
249
|
+
self['sdate'] = ICalPal.nth(nth, dows, nsdate)
|
250
|
+
self['edate'] = ICalPal.nth(nth, dows, nedate)
|
251
|
+
occurrences.push(clone)
|
252
|
+
else
|
253
|
+
if dows[0]
|
254
|
+
self['sdate'] = RDT.new(nsdate.year, m.to_i, nsdate.wday)
|
255
|
+
self['edate'] = RDT.new(nedate.year, m.to_i, nedate.wday)
|
256
|
+
occurrences.push(clone)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
230
260
|
|
231
|
-
|
261
|
+
return(occurrences)
|
232
262
|
end
|
233
263
|
|
234
264
|
# Apply frequency and interval
|
@@ -243,6 +273,7 @@ module ICalPal
|
|
243
273
|
when 'weekly' then self[d] += self['interval'] * 7
|
244
274
|
when 'monthly' then self[d] >>= self['interval']
|
245
275
|
when 'yearly' then self[d] >>= self['interval'] * 12
|
276
|
+
else $log.error("Unknown frequency: #{self['frequency']}")
|
246
277
|
end
|
247
278
|
end if self['frequency'] && self['interval']
|
248
279
|
end
|
data/lib/icalPal.rb
CHANGED
@@ -141,7 +141,7 @@ module ICalPal
|
|
141
141
|
# Get the +n+'th +dow+ in month +m+
|
142
142
|
#
|
143
143
|
# @param n [Integer] Integer between -4 and +4
|
144
|
-
# @param dow [
|
144
|
+
# @param dow [Array] Days of the week
|
145
145
|
# @param m [RDT] The RDT with the year and month we're searching
|
146
146
|
# @return [RDT] The resulting day
|
147
147
|
def self.nth(n, dow, m)
|
@@ -155,7 +155,7 @@ module ICalPal
|
|
155
155
|
|
156
156
|
j = 0
|
157
157
|
a[0].step(a[1], step) do |i|
|
158
|
-
j += step if i.wday
|
158
|
+
j += step if dow.any?(i.wday)
|
159
159
|
return i if j == n
|
160
160
|
end
|
161
161
|
end
|
data/lib/options.rb
CHANGED
@@ -13,7 +13,7 @@ module ICalPal
|
|
13
13
|
#
|
14
14
|
# Many options are intentionally copied from
|
15
15
|
# icalBuddy[https://github.com/ali-rantakari/icalBuddy]. Note that
|
16
|
-
#
|
16
|
+
# icalpal requires two hyphens for all options, except single-letter
|
17
17
|
# options which require a single hyphen.
|
18
18
|
#
|
19
19
|
# Options can be abbreviated as long as they are unique.
|
@@ -55,7 +55,7 @@ module ICalPal
|
|
55
55
|
"Print as FORMAT (default: #{$defaults[:common][:output]})", "[#{OUTFORMATS.join(', ')}]")
|
56
56
|
|
57
57
|
# include/exclude
|
58
|
-
@op.separator("\nIncluding/excluding calendars:\n\n")
|
58
|
+
@op.separator("\nIncluding/excluding accounts, calendars, items:\n\n")
|
59
59
|
|
60
60
|
@op.on('--is=ACCOUNTS', Array, 'List of accounts to include')
|
61
61
|
@op.on('--es=ACCOUNTS', Array, 'List of accounts to exclude')
|
@@ -73,6 +73,9 @@ module ICalPal
|
|
73
73
|
@op.on('--il=LISTS', Array, 'List of reminder lists to include')
|
74
74
|
@op.on('--el=LISTS', Array, 'List of reminder lists to exclude')
|
75
75
|
|
76
|
+
@op.separator('')
|
77
|
+
@op.on('--match=FIELD=REGEXP', String, 'Include only items whose FIELD matches REGEXP (ignoring case)')
|
78
|
+
|
76
79
|
# dates
|
77
80
|
@op.separator("\nChoosing dates:\n\n")
|
78
81
|
|
data/lib/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: icalPal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Rosen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sqlite3
|
@@ -50,6 +50,7 @@ extra_rdoc_files:
|
|
50
50
|
files:
|
51
51
|
- README.md
|
52
52
|
- bin/icalPal
|
53
|
+
- bin/icalpal
|
53
54
|
- icalPal.gemspec
|
54
55
|
- lib/EventKit.rb
|
55
56
|
- lib/ToICalPal.rb
|
@@ -81,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
82
|
- !ruby/object:Gem::Version
|
82
83
|
version: '0'
|
83
84
|
requirements: []
|
84
|
-
rubygems_version: 3.5.
|
85
|
+
rubygems_version: 3.5.18
|
85
86
|
signing_key:
|
86
87
|
specification_version: 4
|
87
88
|
summary: Command-line tool to query the macOS Calendar
|