icalPal 2.0.0 → 2.2.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 +45 -27
- data/bin/icalPal +30 -9
- data/lib/defaults.rb +1 -0
- data/lib/event.rb +87 -63
- data/lib/icalPal.rb +40 -16
- data/lib/options.rb +4 -1
- data/lib/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 76513daaf66f3c4aa8b2a9e1703f275ba5d5a65ef9e8b71a86b263c59d409fdd
|
4
|
+
data.tar.gz: c06430cfbdd5c415dae2d154b774c1ba1feb210534e6511f9fbb1cf12052cbfd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 290f75e744ea88673f474c478565ec281a2b8eae8dcfc730ccb7abf88f33b5fdc8c10baf53b971c336d48a3a4abc3f311e02973e9f2295fea2d134e3d987a499
|
7
|
+
data.tar.gz: af3d7eec48d408f2c7ff15af7cdddcf581fa60574e1ab3602efb32e6625bfa61833919194108fe012fbae35a312c14af6ffbf628be4921d7cd11651e67717151
|
data/README.md
CHANGED
@@ -4,16 +4,30 @@
|
|
4
4
|
|
5
5
|
## Description
|
6
6
|
|
7
|
-
icalPal is a command-line tool to query a macOS Calendar
|
8
|
-
accounts, calendars, and
|
9
|
-
[Ruby](https://www.ruby-lang.org/) and access to a
|
10
|
-
|
7
|
+
icalPal is a command-line tool to query a macOS Calendar and Reminders
|
8
|
+
databases for accounts, calendars, events, and tasks. It can be run
|
9
|
+
on any system with [Ruby](https://www.ruby-lang.org/) and access to a
|
10
|
+
Calendar or Reminders database.
|
11
11
|
|
12
12
|
## Installation
|
13
13
|
|
14
|
+
As a system-wide Ruby gem:
|
15
|
+
|
14
16
|
```
|
15
17
|
gem install icalPal
|
16
|
-
|
18
|
+
```
|
19
|
+
|
20
|
+
or in your home diretory:
|
21
|
+
|
22
|
+
```
|
23
|
+
gem install --user-install icalPal
|
24
|
+
```
|
25
|
+
|
26
|
+
As a Homebrew formula:
|
27
|
+
|
28
|
+
```
|
29
|
+
brew tap ajrosen/icalPal
|
30
|
+
brew install icalPal
|
17
31
|
```
|
18
32
|
|
19
33
|
## Features
|
@@ -21,7 +35,8 @@ icalPal events
|
|
21
35
|
### Compatability with [icalBuddy](https://github.com/ali-rantakari/icalBuddy)
|
22
36
|
|
23
37
|
icalPal tries to be compatible with icalBuddy for command-line options
|
24
|
-
and for output. There are a
|
38
|
+
and for output. There are a some important differences to be aware
|
39
|
+
of.
|
25
40
|
|
26
41
|
* Options require two hyphens, except for single-letter options that require one hyphen
|
27
42
|
* *eventsFrom* is not supported. Instead there is *--from*, *--to*, and *--days*
|
@@ -184,27 +199,6 @@ Environment variables:
|
|
184
199
|
(default: /Users/ajr/.icalPal)
|
185
200
|
```
|
186
201
|
|
187
|
-
## History
|
188
|
-
|
189
|
-
I have used icalBuddy for many years. It's great for scripting,
|
190
|
-
automation, and as a desktop widget for apps like
|
191
|
-
[GeekTool](https://www.tynsoe.org/geektool/) and
|
192
|
-
[Übersicht](https://tracesof.net/uebersicht/).
|
193
|
-
|
194
|
-
As with many applications, I started to run into some limitations in
|
195
|
-
icalBuddy. The biggest being that active development ended in 2014.
|
196
|
-
It's only thanks to the efforts of [Jim
|
197
|
-
Lawton](https://github.com/jimlawton) that it even compiles anymore.
|
198
|
-
|
199
|
-
Instead of trying to understand and extend the existing code, I chose
|
200
|
-
to start anew using my language of choice. Using Ruby means icalPal
|
201
|
-
is multi-platform. It also meant *much* less code; about 1,200 lines
|
202
|
-
vs. 7,000.
|
203
|
-
|
204
|
-
I won't pretend to understand **why** you would want this on Linux or
|
205
|
-
Windows. But since icalPal is written in Ruby and gets its data
|
206
|
-
directly from the Calendar database file instead of an API, you *can*.
|
207
|
-
|
208
202
|
## Output formats
|
209
203
|
|
210
204
|
icalPal supports several output formats. The **default** format tries
|
@@ -242,5 +236,29 @@ objects, one for each of the item's properties:
|
|
242
236
|
|
243
237
|
The document will also include a number of
|
244
238
|
[RDoc::Markup::Verbatim](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Verbatim.html)
|
239
|
+
and
|
240
|
+
[RDoc::Markup::Raw](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Raw.html)
|
245
241
|
items. They are not included in the output, but are used to pass
|
246
242
|
information about the item and property to the default formatter.
|
243
|
+
|
244
|
+
## History
|
245
|
+
|
246
|
+
I used icalBuddy for many years. It's great for scripting,
|
247
|
+
automation, and as a desktop widget for apps like
|
248
|
+
[GeekTool](https://www.tynsoe.org/geektool/) and
|
249
|
+
[Übersicht](https://tracesof.net/uebersicht/).
|
250
|
+
|
251
|
+
As with many applications, I started to run into some limitations in
|
252
|
+
icalBuddy. The biggest being that active development ended in 2014.
|
253
|
+
It's only thanks to the efforts of [Jim
|
254
|
+
Lawton](https://github.com/jimlawton) that it even compiles anymore.
|
255
|
+
|
256
|
+
Instead of trying to understand and extend the existing code, I chose
|
257
|
+
to start anew using my language of choice: Ruby. Using Ruby meant
|
258
|
+
there is *much* less code; about 1,600 lines vs. 7,000. It also means
|
259
|
+
icalPal is multi-platform.
|
260
|
+
|
261
|
+
I won't pretend to understand **why** you would want to run this on
|
262
|
+
Linux or Windows. But since icalPal is written in Ruby and gets its
|
263
|
+
data directly from the Calendar and Reminders database files instead
|
264
|
+
of an API, you *can*.
|
data/bin/icalPal
CHANGED
@@ -1,16 +1,25 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
# *-*- mode: enh-ruby -*-*
|
3
2
|
|
4
|
-
|
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]
|
5
16
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
require 'sqlite3'
|
10
|
-
require 'yaml'
|
17
|
+
$stderr.puts "FATAL: icalPal is missing a dependency: #{dep}"
|
18
|
+
$stderr.puts
|
19
|
+
$stderr.puts "Install with 'gem install --user-install #{dep}'"
|
11
20
|
|
12
|
-
|
13
|
-
|
21
|
+
exit
|
22
|
+
end
|
14
23
|
|
15
24
|
|
16
25
|
##################################################
|
@@ -121,6 +130,18 @@ $rows.each_with_index do |row, i|
|
|
121
130
|
next
|
122
131
|
end
|
123
132
|
|
133
|
+
# --regexp
|
134
|
+
if $opts[:match]
|
135
|
+
r = $opts[:match].split('=')
|
136
|
+
|
137
|
+
if item[r[0]].to_s.respond_to?(:match)
|
138
|
+
unless item[r[0]].to_s.match(Regexp.new(r[1].to_s, Regexp::IGNORECASE)) then
|
139
|
+
$log.debug(":regex")
|
140
|
+
next
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
124
145
|
if ICalPal::Event === item
|
125
146
|
# Check for all-day and cancelled events
|
126
147
|
if $opts[:ea] && item['all_day'].positive? then
|
data/lib/defaults.rb
CHANGED
data/lib/event.rb
CHANGED
@@ -110,18 +110,22 @@ module ICalPal
|
|
110
110
|
# If an event spans multiple days, the return value will contain
|
111
111
|
# a unique {Event} for each day that falls within our window
|
112
112
|
def non_recurring
|
113
|
-
|
113
|
+
events = []
|
114
114
|
|
115
115
|
# Repeat for multi-day events
|
116
116
|
((self['duration'] / 86400).to_i + 1).times do |i|
|
117
|
+
break if self['sdate'] > $opts[:to]
|
118
|
+
|
117
119
|
$log.debug("multi-day event #{i + 1}") if (i > 0)
|
120
|
+
|
118
121
|
self['daynum'] = i + 1
|
119
|
-
|
122
|
+
events.push(clone) if in_window?(self['sdate'])
|
123
|
+
|
120
124
|
self['sdate'] += 1
|
121
125
|
self['edate'] += 1
|
122
126
|
end
|
123
127
|
|
124
|
-
|
128
|
+
events
|
125
129
|
end
|
126
130
|
|
127
131
|
# Check recurring events
|
@@ -129,35 +133,49 @@ module ICalPal
|
|
129
133
|
# @return [Array<Event>]
|
130
134
|
# All occurrences of a recurring event that are within our window
|
131
135
|
def recurring
|
132
|
-
|
136
|
+
stop = [ $opts[:to], (self['rdate'] || $opts[:to]) ].min
|
133
137
|
|
134
138
|
# See if event ends before we start
|
135
|
-
stop = [ $opts[:to], (self['rdate'] || $opts[:to]) ].min
|
136
139
|
if stop < $opts[:from] then
|
137
140
|
$log.debug("#{stop} < #{$opts[:from]}")
|
138
|
-
return(
|
141
|
+
return(Array.new)
|
139
142
|
end
|
140
143
|
|
141
144
|
# Get changes to series
|
142
|
-
changes =
|
145
|
+
changes = [ { 'orig_date' => -1 } ]
|
146
|
+
changes += $rows.select { |r| r['orig_item_id'] == self['ROWID'] }
|
147
|
+
|
148
|
+
events = []
|
149
|
+
count = 1
|
143
150
|
|
144
|
-
i = 1
|
145
151
|
while self['sdate'] <= stop
|
146
|
-
|
147
|
-
|
148
|
-
|
152
|
+
# count
|
153
|
+
break if self['count'].positive? and count > self['count']
|
154
|
+
count += 1
|
155
|
+
|
156
|
+
# Handle specifier or clone self
|
157
|
+
if self['specifier'] and self['specifier'].length.positive?
|
158
|
+
occurrences = get_occurrences(changes)
|
159
|
+
else
|
160
|
+
occurrences = [ clone ]
|
149
161
|
end
|
150
|
-
i += 1
|
151
162
|
|
152
|
-
|
153
|
-
|
154
|
-
|
163
|
+
# Check for changes
|
164
|
+
occurrences.each do |occurrence|
|
165
|
+
changes.each do |change|
|
166
|
+
next if change['orig_date'] == self['sdate'].to_i - ITIME
|
167
|
+
events.push(occurrence) if in_window?(occurrence['sdate'], occurrence['edate'])
|
168
|
+
end
|
155
169
|
end
|
156
170
|
|
171
|
+
break if self['specifier']
|
157
172
|
apply_frequency!
|
158
173
|
end
|
159
174
|
|
160
|
-
|
175
|
+
# Remove exceptions
|
176
|
+
events.delete_if { |event| event['xdate'].any?(event['sdate']) }
|
177
|
+
|
178
|
+
return(events)
|
161
179
|
end
|
162
180
|
|
163
181
|
private
|
@@ -166,67 +184,72 @@ module ICalPal
|
|
166
184
|
|
167
185
|
# @return a deep clone of self
|
168
186
|
def clone()
|
169
|
-
self['stime'] = @self['sdate'].to_i
|
170
|
-
self['etime'] = @self['edate'].to_i
|
171
|
-
|
172
187
|
Marshal.load(Marshal.dump(self))
|
173
188
|
end
|
174
189
|
|
175
|
-
# Get next occurences of a recurring event
|
190
|
+
# Get next occurences of a recurring event from a specifier
|
176
191
|
#
|
177
192
|
# @param changes [Array] Recurrence changes for the event
|
178
193
|
# @return [Array<IcalPal::Event>]
|
179
194
|
def get_occurrences(changes)
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
195
|
+
occurrences = []
|
196
|
+
|
197
|
+
dow = DOW.keys
|
198
|
+
dom = [ nil ]
|
199
|
+
moy = 1..12
|
200
|
+
nth = nil
|
201
|
+
|
202
|
+
specifier = self['specifier']? self['specifier'] : []
|
203
|
+
|
204
|
+
# Deconstruct specifier
|
205
|
+
specifier.split(';').each do |k|
|
206
|
+
j = k.split('=')
|
207
|
+
|
208
|
+
# D=Day of the week, M=Day of the month, O=Month of the year, S=Nth
|
209
|
+
case j[0]
|
210
|
+
when 'D' then dow = j[1].split(',')
|
211
|
+
when 'M' then dom = j[1].split(',')
|
212
|
+
when 'O' then moy = j[1].split(',')
|
213
|
+
when 'S' then nth = j[1].to_i
|
214
|
+
else $log.warn("Unknown specifier: #{k}")
|
198
215
|
end
|
216
|
+
end
|
199
217
|
|
200
|
-
|
201
|
-
|
202
|
-
|
218
|
+
# Build array of DOWs
|
219
|
+
dows = [ nil ]
|
220
|
+
dow.each { |d| dows.push(DOW[d[-2..-1].to_sym]) }
|
203
221
|
|
204
|
-
|
205
|
-
|
206
|
-
|
222
|
+
# Months of the year (O)
|
223
|
+
moy.each do |m|
|
224
|
+
next unless m
|
207
225
|
|
208
|
-
|
209
|
-
|
210
|
-
dow = DOW[n[-2..-1].to_sym]
|
211
|
-
ndate += 1 until ndate.wday == dow
|
212
|
-
ndate = ICalPal.nth(Integer(n[0..1]), n[-2..-1], ndate) unless (n[0] == '0')
|
226
|
+
nsdate = RDT.new(self['sdate'].year, m.to_i, 1)
|
227
|
+
nedate = RDT.new(self['edate'].year, m.to_i, 1)
|
213
228
|
|
214
|
-
#
|
215
|
-
|
216
|
-
|
217
|
-
self['sdate'] = RDT.new(*ndate.to_a[0..2], *self['sdate'].to_a[3..])
|
218
|
-
self['edate'] = RDT.new(*ndate.to_a[0..2], *self['edate'].to_a[3..])
|
219
|
-
retval.push(clone)
|
220
|
-
}) { |i| @self['sday'] == i['sday'] }
|
221
|
-
end
|
229
|
+
# Days of the month (M)
|
230
|
+
dom.each do |x|
|
231
|
+
next unless x
|
222
232
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
233
|
+
self['sdate'] = RDT.new(nsdate.year, nsdate.month, x.to_i)
|
234
|
+
self['edate'] = RDT.new(nedate.year, nedate.month, x.to_i)
|
235
|
+
occurrences.push(clone)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Days of the week (D)
|
239
|
+
if nth
|
240
|
+
self['sdate'] = ICalPal.nth(nth, dows, nsdate)
|
241
|
+
self['edate'] = ICalPal.nth(nth, dows, nedate)
|
242
|
+
occurrences.push(clone)
|
243
|
+
else
|
244
|
+
if dows[0]
|
245
|
+
self['sdate'] = RDT.new(nsdate.year, m.to_i, nsdate.wday)
|
246
|
+
self['edate'] = RDT.new(nedate.year, m.to_i, nedate.wday)
|
247
|
+
occurrences.push(clone)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
228
251
|
|
229
|
-
|
252
|
+
return(occurrences)
|
230
253
|
end
|
231
254
|
|
232
255
|
# Apply frequency and interval
|
@@ -241,6 +264,7 @@ module ICalPal
|
|
241
264
|
when 'weekly' then self[d] += self['interval'] * 7
|
242
265
|
when 'monthly' then self[d] >>= self['interval']
|
243
266
|
when 'yearly' then self[d] >>= self['interval'] * 12
|
267
|
+
else $log.error("Unknown frequency: #{self['frequency']}")
|
244
268
|
end
|
245
269
|
end if self['frequency'] && self['interval']
|
246
270
|
end
|
data/lib/icalPal.rb
CHANGED
@@ -58,6 +58,9 @@ module ICalPal
|
|
58
58
|
rescue SQLite3::BusyException => e
|
59
59
|
$log.error("Non-fatal error closing database #{db.filename}")
|
60
60
|
|
61
|
+
rescue SQLite3::SQLException => e
|
62
|
+
$log.info("#{db_file}: #{e}")
|
63
|
+
|
61
64
|
rescue SQLite3::Exception => e
|
62
65
|
abort("#{db_file}: #{e}")
|
63
66
|
|
@@ -92,32 +95,53 @@ module ICalPal
|
|
92
95
|
CSV::Row::new(headers, values)
|
93
96
|
end
|
94
97
|
|
98
|
+
# Convert +self+ to XML
|
99
|
+
#
|
95
100
|
# @return [String] All fields in a simple XML format: <field>value</field>.
|
96
101
|
# Fields with empty values return <field/>.
|
97
102
|
def to_xml
|
98
103
|
retval = ""
|
99
|
-
|
100
|
-
@self.keys.each do |k|
|
101
|
-
v = @self[k]
|
102
|
-
|
103
|
-
if v.respond_to?(:length) then
|
104
|
-
if v.length == 0 or v[0] == nil then
|
105
|
-
retval += "<#{k}/>"
|
106
|
-
else
|
107
|
-
# Keep non-blank and whitespace, except form feeds and vertical whitespace
|
108
|
-
v = v.gsub(/[^[[:print:]][[:space:]]]/, '.').gsub(/[\f\v]/, '.')
|
109
|
-
retval += "<#{k}>#{v}</#{k}>"
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
104
|
+
@self.keys.each { |k| retval += xmlify(k, @self[k]) }
|
113
105
|
|
114
106
|
retval
|
115
107
|
end
|
116
108
|
|
109
|
+
# Convert a key/value pair to XML. The value should be +nil+, +String+,
|
110
|
+
# +Integer+, +Array+, or +ICalPal::RDT+
|
111
|
+
#
|
112
|
+
# @param key The key
|
113
|
+
# @param value The value
|
114
|
+
# @return [String] The key/value pair in a simple XML format
|
115
|
+
def xmlify(key, value)
|
116
|
+
case value
|
117
|
+
# Nil
|
118
|
+
when NilClass then return("<#{key}/>")
|
119
|
+
|
120
|
+
# String, Integer
|
121
|
+
when String then return("<#{key}>#{value}</#{key}>")
|
122
|
+
when Integer then return("<#{key}>#{value}</#{key}>")
|
123
|
+
|
124
|
+
# Array
|
125
|
+
when Array then
|
126
|
+
# Treat empty arrays as nil values
|
127
|
+
return(xmlify(key, nil)) if value[0] == nil
|
128
|
+
|
129
|
+
retval = ""
|
130
|
+
value.each { |x| retval += xmlify("#{key}0", x) }
|
131
|
+
return("<#{key}>#{retval}</#{key}>")
|
132
|
+
|
133
|
+
# RDT
|
134
|
+
when ICalPal::RDT then return("<#{key}>#{value.to_s}</#{key}>")
|
135
|
+
|
136
|
+
# Unknown
|
137
|
+
else return("<#{key}>#{value.to_s}</#{key}>")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
117
141
|
# Get the +n+'th +dow+ in month +m+
|
118
142
|
#
|
119
143
|
# @param n [Integer] Integer between -4 and +4
|
120
|
-
# @param dow [
|
144
|
+
# @param dow [Array] Days of the week
|
121
145
|
# @param m [RDT] The RDT with the year and month we're searching
|
122
146
|
# @return [RDT] The resulting day
|
123
147
|
def self.nth(n, dow, m)
|
@@ -131,7 +155,7 @@ module ICalPal
|
|
131
155
|
|
132
156
|
j = 0
|
133
157
|
a[0].step(a[1], step) do |i|
|
134
|
-
j += step if i.wday
|
158
|
+
j += step if dow.any?(i.wday)
|
135
159
|
return i if j == n
|
136
160
|
end
|
137
161
|
end
|
data/lib/options.rb
CHANGED
@@ -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: 2.
|
4
|
+
version: 2.2.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-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sqlite3
|
@@ -81,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
requirements: []
|
84
|
-
rubygems_version: 3.5.
|
84
|
+
rubygems_version: 3.5.9
|
85
85
|
signing_key:
|
86
86
|
specification_version: 4
|
87
87
|
summary: Command-line tool to query the macOS Calendar
|