icalPal 2.1.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4dab143a05d8a63ae36b721ca6ba53180c75d8b4dd0f0cebdc84fb533968c248
4
- data.tar.gz: 8d243a120762c90059e4206a2deb5e79b821e558cf9ad9a437ad84fbfbb6a21b
3
+ metadata.gz: 25a711f9e4887a81dafccbc57e0a2fa84530dc8cee81e58f0d5df6041783bec2
4
+ data.tar.gz: 698a498065687f7a7d06e6c316d1937bfc3b1a58d7a661b612bb8d39b5429318
5
5
  SHA512:
6
- metadata.gz: 24f4b88c592e59f2ffbc116761d6da41b60a7e5d4554feea9bdb023450967eaa7921f5c4708a66bf76554926ab4e0ca52965470f7576cef1cce5aec9a6112cbb
7
- data.tar.gz: d69cc1d2cd63072020a4059a352ccdb2262a189e10c3c4e1891bd1c953ea2cb60a577fc83bd2ab55b561d0a752b0fb4198495bc2a5f57909d4216101cc7ea012
6
+ metadata.gz: 9cf18231c95a94b663e1a41ecd16326f447fb038864677ad05ba2a6fa5f684ae999732401ff30b9df1b0739ff31f78f7b0385e3f7774d5260b9cfbc37a6d2680
7
+ data.tar.gz: d3aaf37df004da104518e80463c2bc6933ba87277fc9c913bb73a4653055a93417694fb1de655d1c3776f3d47d45afaf241b5ba59361894bafa7ebd1bf253c29
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- [![Gem Version](https://badge.fury.io/rb/icalPal.svg)](https://badge.fury.io/rb/icalPal)
1
+ [![Gem Version](https://badge.fury.io/rb/icalpal.svg)](https://badge.fury.io/rb/icalpal)
2
2
 
3
- # icalPal
3
+ # icalpal
4
4
 
5
5
  ## Description
6
6
 
7
- icalPal is a command-line tool to query a macOS Calendar and Reminders
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 icalPal
17
+ gem install icalpal
18
18
  ```
19
19
 
20
20
  or in your home diretory:
21
21
 
22
22
  ```
23
- gem install --user-install icalPal
23
+ gem install --user-install icalpal
24
24
  ```
25
25
 
26
26
  As a Homebrew formula:
27
27
 
28
28
  ```
29
- brew tap ajrosen/icalPal
30
- brew install icalPal
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
- icalPal tries to be compatible with icalBuddy for command-line options
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
- ```icalPal accounts```
51
+ ```icalpal accounts```
52
52
 
53
- Shows a list of enabled Calendar accounts. Internally they are known as *Stores*; you can run ```icalPal stores``` instead.
53
+ Shows a list of enabled Calendar accounts. Internally they are known as *Stores*; you can run ```icalpal stores``` instead.
54
54
 
55
- ```icalPal datedTasks```
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., ```icalPal -c ev --da 3``` is the same as ```icalPal -c events --days 3```.
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 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
+ 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
- icalPal: Usage: icalPal [options] [-c] COMMAND
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/.icalPal)
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/.icalPal)
203
+ (default: /Users/ajr/.icalpal)
200
204
  ```
201
205
 
202
206
  ## Output formats
203
207
 
204
- icalPal supports several output formats. The **default** format tries
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 icalPal.
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
- icalPal is multi-platform.
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 icalPal is written in Ruby and gets its
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/icalPal'
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: icalPal is missing a dependency: #{dep}"
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
- if $opts[:ec].any? row['calendar'] then
99
- $log.debug(":ec")
100
- next
101
- end
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
- unless $opts[:ic].empty? or $opts[:ic].any? row['calendar']
104
- $log.debug(":ic")
105
- next
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 = "icalPal"
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
@@ -42,6 +42,7 @@ class EventKit
42
42
  { name: 'MobileMe', color: '#FFFF00' }, # Yellow
43
43
  { name: 'Subscribed', color: '#FF0000' }, # Red
44
44
  { name: 'Birthdays', color: '#FF00FF' }, # Magenta
45
+ { name: 'Reminders', color: '#066FF3' }, # Blue
45
46
  ]
46
47
 
47
48
  EKSpan = [
data/lib/defaults.rb CHANGED
@@ -8,7 +8,7 @@ $defaults = {
8
8
  ab: '!',
9
9
  aep: [],
10
10
  bullet: '•',
11
- cf: "#{ENV['HOME']}/.icalPal",
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
- @self['sdate'] = RDT.new(*(@self['sdate'].to_time - Time.zone_offset($now.zone())).to_a.reverse[4..], $now.zone)
96
- @self['edate'] = RDT.new(*(@self['edate'].to_time - Time.zone_offset($now.zone())).to_a.reverse[4..], $now.zone)
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
- retval = []
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
- ((self['duration'] / 86400).to_i + 1).times do |i|
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
- retval.push(clone) if in_window?(self['sdate'])
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
- retval
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
- retval = []
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(retval)
150
+ return(Array.new)
141
151
  end
142
152
 
143
153
  # Get changes to series
144
- changes = $rows.select { |r| r['orig_item_id'] == self['ROWID'] }
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
- if self['count'].positive? && i > self['count'] then
149
- $log.debug("count exceeded: #{i} > #{self['count']}")
150
- return(retval)
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
- unless @self['xdate'].any?(@self['sdate']) # Exceptions?
155
- o = get_occurrences(changes)
156
- o.each { |r| retval.push(r) if in_window?(r['sdate'], r['edate']) }
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
- retval
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
- ndate = self['sdate']
183
- odays = []
184
- retval = []
185
-
186
- # Deconstruct specifier(s)
187
- if self['specifier']
188
- self['specifier'].split(';').each do |k|
189
- j = k.split('=')
190
-
191
- # M=Day of the month, O=Month of the year, S=Nth
192
- case j[0]
193
- when 'M' then ndate = RDT.new(ndate.year, ndate.month, j[1].to_i)
194
- when 'O' then ndate = RDT.new(ndate.year, j[1].to_i, ndate.day)
195
- when 'S' then @self['specifier'].sub!(/D=0/, "D=+#{j[1].to_i}")
196
- end
197
-
198
- # No time travel!
199
- ndate = self['sdate'] if ndate <= self['sdate']
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
- # D=Day of the week
203
- self['specifier'].split(';').each do |k|
204
- j = k.split('=')
227
+ # Build array of DOWs
228
+ dows = [ nil ]
229
+ dow.each { |d| dows.push(DOW[d[-2..-1].to_sym]) }
205
230
 
206
- odays = j[1].split(',') if j[0] == 'D'
207
- end
208
- end
231
+ # Months of the year (O)
232
+ moy.each do |m|
233
+ next unless m
209
234
 
210
- # Deconstruct occurence day(s)
211
- odays.each do |n|
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
- # Check for changes
217
- changes.detect(
218
- proc {
219
- self['sdate'] = RDT.new(*ndate.to_a[0..2], *self['sdate'].to_a[3..])
220
- self['edate'] = RDT.new(*ndate.to_a[0..2], *self['edate'].to_a[3..])
221
- retval.push(clone)
222
- }) { |i| @self['sday'] == i['sday'] }
223
- end
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
- # Check for changes
226
- changes.detect(
227
- proc {
228
- retval.push(clone)
229
- }) { |i| @self['sday'] == i['sday'] } unless retval.count.positive?
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
- retval
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 [String] Day of the week abbreviation from ICalPal::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 == DOW[dow.to_sym]
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
- # icalPal requires two hyphens for all options, except single-letter
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
@@ -1,3 +1,4 @@
1
1
  module ICalPal
2
- VERSION = '2.1.0'
2
+ NAME = 'icalPal'
3
+ VERSION = '3.0.0'
3
4
  end
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.1.0
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-04-28 00:00:00.000000000 Z
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.9
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