icalPal 2.0.0 → 2.2.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: 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