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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f91434d02dd357bcd02342b2d79418b91e23ec789a6a069a01f2f508685d8ca
4
- data.tar.gz: 51cfa58c767650d0c7e62213ad4a58dbe9a2d5fc42f17d5ce18c8b65e6a178be
3
+ metadata.gz: 76513daaf66f3c4aa8b2a9e1703f275ba5d5a65ef9e8b71a86b263c59d409fdd
4
+ data.tar.gz: c06430cfbdd5c415dae2d154b774c1ba1feb210534e6511f9fbb1cf12052cbfd
5
5
  SHA512:
6
- metadata.gz: 20b4a10406535baf33986bd6bdf859376a6dfd7055815b2f263d6971d6e8255d5d60fa019c49c4de54ec3a4178e3752136478aafa5937176c80adfdfc36cc8aa
7
- data.tar.gz: 8a5fbde979186c3347ed6c8c52f3b39b81fc6b303680e1923c0b011583b1c63651dc65b33e4e34a3794b5d01430cb7bd4dd66b2f12510374f13626e438aef079
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 database for
8
- accounts, calendars, and events. It can be run on any system with
9
- [Ruby](https://www.ruby-lang.org/) and access to a Calendar database
10
- file, or a Reminders database.
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
- icalPal events
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 few differences to be aware of.
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
- require 'logger'
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
- require 'csv'
7
- require 'json'
8
- require 'rdoc'
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
- require_relative '../lib/icalPal'
13
- require_relative '../lib/options'
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
@@ -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
@@ -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
- retval = []
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
- retval.push(clone) if in_window?(self['sdate'])
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
- retval
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
- retval = []
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(retval)
141
+ return(Array.new)
139
142
  end
140
143
 
141
144
  # Get changes to series
142
- changes = $rows.select { |r| r['orig_item_id'] == self['ROWID'] }
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
- if self['count'].positive? && i > self['count'] then
147
- $log.debug("count exceeded: #{i} > #{self['count']}")
148
- return(retval)
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
- unless @self['xdate'].any?(@self['sdate']) # Exceptions?
153
- o = get_occurrences(changes)
154
- o.each { |r| retval.push(r) if in_window?(r['sdate'], r['edate']) }
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
- retval
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
- ndate = self['sdate']
181
- odays = []
182
- retval = []
183
-
184
- # Deconstruct specifier(s)
185
- if self['specifier']
186
- self['specifier'].split(';').each do |k|
187
- j = k.split('=')
188
-
189
- # M=Day of the month, O=Month of the year, S=Nth
190
- case j[0]
191
- when 'M' then ndate = RDT.new(ndate.year, ndate.month, j[1].to_i)
192
- when 'O' then ndate = RDT.new(ndate.year, j[1].to_i, ndate.day)
193
- when 'S' then @self['specifier'].sub!(/D=0/, "D=+#{j[1].to_i}")
194
- end
195
-
196
- # No time travel!
197
- ndate = self['sdate'] if ndate <= self['sdate']
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
- # D=Day of the week
201
- self['specifier'].split(';').each do |k|
202
- j = k.split('=')
218
+ # Build array of DOWs
219
+ dows = [ nil ]
220
+ dow.each { |d| dows.push(DOW[d[-2..-1].to_sym]) }
203
221
 
204
- odays = j[1].split(',') if j[0] == 'D'
205
- end
206
- end
222
+ # Months of the year (O)
223
+ moy.each do |m|
224
+ next unless m
207
225
 
208
- # Deconstruct occurence day(s)
209
- odays.each do |n|
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
- # Check for changes
215
- changes.detect(
216
- proc {
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
- # Check for changes
224
- changes.detect(
225
- proc {
226
- retval.push(clone)
227
- }) { |i| @self['sday'] == i['sday'] } unless retval.count.positive?
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
- retval
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 [String] Day of the week abbreviation from ICalPal::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 == DOW[dow.to_sym]
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
@@ -1,3 +1,3 @@
1
1
  module ICalPal
2
- VERSION = '2.0.0'
2
+ VERSION = '2.2.0'
3
3
  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.0.0
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-04-14 00:00:00.000000000 Z
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.4
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