icalPal 3.9.1 → 3.9.3

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: a3c3807a9fd8957753d06626cb7fa9d2fa34853352a4e28ace5302d8abcf706d
4
- data.tar.gz: 8f9c792ca674a18037e89eed786f4c66db91b0e1105ba483d66fb73710281f79
3
+ metadata.gz: 4853d1f050b001c63c1890618f5393eaee86c812f86cacf8be8b9da483fc67d1
4
+ data.tar.gz: ae170e19546fc153bdfbcd6cd89e02ac91c694e2c393e6624e31d437a5b03a34
5
5
  SHA512:
6
- metadata.gz: 0b21f98466467cd6b2e2f518f3d5591c5e3b0abd6c1562b482950294512a3e0e57e2d95a2de96581886ac80b6da6d46d59cef01942684602018af6c7f05a5fa0
7
- data.tar.gz: '0209a037605af52065d6727a2dd9ec06ddc9028ff801de2da5c4eb6825ad8aceb1a4a58747a172f22db85eee25d1b1724f9f4f337fe32473df6c604f77603d2e'
6
+ metadata.gz: 3bf5728beb2504a19f2fae851413c26c6ee192fe2f09e066f8f9b1887e3eb16102eb9a89a4eb5e717e727564e8ca4a74bd09dc3d2743974cf2d33a24d7b25b5d
7
+ data.tar.gz: b60b90e37a562ca7df73c53e0e6ade623baa857b3f520a7faded502e7d588c7cbcb3ef2ee02bc9f486bd840f11b4e7638d92f769570e6b216e3bff9b2eaae5af
data/README.md CHANGED
@@ -32,12 +32,19 @@ As a system-wide Ruby gem:
32
32
  gem install icalPal
33
33
  ```
34
34
 
35
- or in your home diretory:
35
+ In your home directory:
36
36
 
37
37
  ```
38
38
  gem install --user-install icalPal
39
39
  ```
40
40
 
41
+ As a [Homebrew](https://brew.sh) formula:
42
+
43
+ ```
44
+ brew tap ajrosen/tap
45
+ brew install icalPal
46
+ ```
47
+
41
48
  ## Features
42
49
 
43
50
  ### Compatability with icalBuddy
@@ -280,11 +287,12 @@ to mimic icalBuddy as much as possible.
280
287
 
281
288
  CSV, Hash, JSON, XML, and YAML print all fields for all items in their
282
289
  respective formats. From that you can analyze the results any way you
283
- like.
284
-
285
- [Remind](https://dianne.skoll.ca/projects/remind/) format uses a
290
+ like. [Remind](https://dianne.skoll.ca/projects/remind/) format uses a
286
291
  minimal implementation built into icalPal.
287
292
 
293
+ Control characters are escaped in these formats to ensure they remain
294
+ properly formatted.
295
+
288
296
  Other formats such as ANSI, HTML, Markdown, RDoc, and TOC, use Ruby's
289
297
  [RDoc::Markup](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup.html)
290
298
  framework to build and render the items.
data/bin/icalPal CHANGED
@@ -13,7 +13,7 @@ def r(gem)
13
13
  $stderr.puts "FATAL: icalPal is missing a dependency: #{gem}"
14
14
  $stderr.puts e
15
15
  $stderr.puts
16
- abort "Try installing with 'gem install --user-install #{gem}'"
16
+ abort "Try installing with 'gem install #{gem}'"
17
17
  end
18
18
  end
19
19
 
@@ -63,7 +63,7 @@ $items = [] # Items to be printed
63
63
  ##################################################
64
64
  # All kids love log!
65
65
 
66
- $log.info("Options: #{$opts}")
66
+ $log.info("Options:\n#{$opts.to_json}")
67
67
 
68
68
 
69
69
  ##################################################
@@ -90,39 +90,38 @@ $opts[:db].each do |db|
90
90
  $log.debug("Trying #{db}")
91
91
 
92
92
  if klass == ICalPal::Reminder
93
- begin
94
- # Load all .sqlite files
95
- $log.debug("Loading *.sqlite in #{db}")
96
- Dir.glob("#{db}/*.sqlite").each do |d|
97
- success = true
93
+ # Load all .sqlite files
94
+ $log.debug("Loading *.sqlite in #{db}")
95
+ Dir.glob("#{db}/*.sqlite").each do |d|
96
+ success = true
98
97
 
99
- rows = klass.load_data(d, klass::QUERY)
100
- $rows += rows
98
+ rows = klass.load_data(d, klass::QUERY)
99
+ $rows += rows
101
100
 
102
- sections = klass.load_data(d, klass::SECTIONS_QUERY)
103
- $sections += sections
101
+ sections = klass.load_data(d, klass::SECTIONS_QUERY)
102
+ $sections += sections
104
103
 
105
- $log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
106
- rescue SQLite3::CantOpenException
107
- # Non-fatal exception, try the next one
108
- end
109
-
110
- rescue Errno::EPERM
111
- # Probably need Full Disk Access
104
+ $log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
112
105
  end
113
106
  else
114
107
  # Load database
115
- begin
116
- rows = ICalPal.load_data(db, klass::QUERY)
117
- $rows += rows
108
+ rows = ICalPal.load_data(db, klass::QUERY)
109
+ $rows += rows
118
110
 
119
- success = true
111
+ success = true
120
112
 
121
- $log.info("Loaded #{rows.length} rows from #{db}")
122
- rescue SQLite3::CantOpenException
123
- # Non-fatal exception, try the next one
124
- end
113
+ $log.info("Loaded #{rows.length} rows from #{db}")
125
114
  end
115
+
116
+ rescue Errno::EPERM
117
+ # Probably need Full Disk Access
118
+
119
+ rescue SQLite3::CantOpenException
120
+ # Non-fatal exception, try the next one
121
+
122
+ rescue StandardError => e
123
+ # Log the error and (try to) continue
124
+ $log.error("#{db}: #{e.message}")
126
125
  end
127
126
 
128
127
  # Make sure we opened at least one database
@@ -153,6 +152,7 @@ unless success
153
152
  abort
154
153
  end
155
154
 
155
+ $log.debug("Loaded #{$rows.length} #{klass} items")
156
156
  $log.info("Window is #{$opts[:from]} to #{$opts[:to]}") if $opts[:from]
157
157
 
158
158
 
@@ -290,7 +290,7 @@ unless mu
290
290
  when 'remind' then items.map { |i|
291
291
  "REM #{i['sdate'].strftime('%F AT %R')} " +
292
292
  "DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i} " +
293
- "MSG #{i['title']}"
293
+ "MSG #{i['title'].gsub(/([[:cntrl:]])/) { |c| c.dump[1..-2] }}"
294
294
  }.join("\n")
295
295
  else abort "No formatter for #{$opts[:output]}"
296
296
  end
data/bin/icalpal CHANGED
@@ -13,7 +13,7 @@ def r(gem)
13
13
  $stderr.puts "FATAL: icalPal is missing a dependency: #{gem}"
14
14
  $stderr.puts e
15
15
  $stderr.puts
16
- abort "Try installing with 'gem install --user-install #{gem}'"
16
+ abort "Try installing with 'gem install #{gem}'"
17
17
  end
18
18
  end
19
19
 
@@ -63,7 +63,7 @@ $items = [] # Items to be printed
63
63
  ##################################################
64
64
  # All kids love log!
65
65
 
66
- $log.info("Options: #{$opts}")
66
+ $log.info("Options:\n#{$opts.to_json}")
67
67
 
68
68
 
69
69
  ##################################################
@@ -90,39 +90,38 @@ $opts[:db].each do |db|
90
90
  $log.debug("Trying #{db}")
91
91
 
92
92
  if klass == ICalPal::Reminder
93
- begin
94
- # Load all .sqlite files
95
- $log.debug("Loading *.sqlite in #{db}")
96
- Dir.glob("#{db}/*.sqlite").each do |d|
97
- success = true
93
+ # Load all .sqlite files
94
+ $log.debug("Loading *.sqlite in #{db}")
95
+ Dir.glob("#{db}/*.sqlite").each do |d|
96
+ success = true
98
97
 
99
- rows = klass.load_data(d, klass::QUERY)
100
- $rows += rows
98
+ rows = klass.load_data(d, klass::QUERY)
99
+ $rows += rows
101
100
 
102
- sections = klass.load_data(d, klass::SECTIONS_QUERY)
103
- $sections += sections
101
+ sections = klass.load_data(d, klass::SECTIONS_QUERY)
102
+ $sections += sections
104
103
 
105
- $log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
106
- rescue SQLite3::CantOpenException
107
- # Non-fatal exception, try the next one
108
- end
109
-
110
- rescue Errno::EPERM
111
- # Probably need Full Disk Access
104
+ $log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
112
105
  end
113
106
  else
114
107
  # Load database
115
- begin
116
- rows = ICalPal.load_data(db, klass::QUERY)
117
- $rows += rows
108
+ rows = ICalPal.load_data(db, klass::QUERY)
109
+ $rows += rows
118
110
 
119
- success = true
111
+ success = true
120
112
 
121
- $log.info("Loaded #{rows.length} rows from #{db}")
122
- rescue SQLite3::CantOpenException
123
- # Non-fatal exception, try the next one
124
- end
113
+ $log.info("Loaded #{rows.length} rows from #{db}")
125
114
  end
115
+
116
+ rescue Errno::EPERM
117
+ # Probably need Full Disk Access
118
+
119
+ rescue SQLite3::CantOpenException
120
+ # Non-fatal exception, try the next one
121
+
122
+ rescue StandardError => e
123
+ # Log the error and (try to) continue
124
+ $log.error("#{db}: #{e.message}")
126
125
  end
127
126
 
128
127
  # Make sure we opened at least one database
@@ -153,6 +152,7 @@ unless success
153
152
  abort
154
153
  end
155
154
 
155
+ $log.debug("Loaded #{$rows.length} #{klass} items")
156
156
  $log.info("Window is #{$opts[:from]} to #{$opts[:to]}") if $opts[:from]
157
157
 
158
158
 
@@ -290,7 +290,7 @@ unless mu
290
290
  when 'remind' then items.map { |i|
291
291
  "REM #{i['sdate'].strftime('%F AT %R')} " +
292
292
  "DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i} " +
293
- "MSG #{i['title']}"
293
+ "MSG #{i['title'].gsub(/([[:cntrl:]])/) { |c| c.dump[1..-2] }}"
294
294
  }.join("\n")
295
295
  else abort "No formatter for #{$opts[:output]}"
296
296
  end
data/ext/extconf.rb CHANGED
@@ -7,7 +7,7 @@ begin
7
7
  Gem.path.each { |p| gemdir = p if File.writable? p }
8
8
 
9
9
  # Dependencies common to all environments
10
- dependencies = %w[ plist timezone ]
10
+ dependencies = %w[ plist tzinfo ]
11
11
 
12
12
  if RUBY_VERSION >= '3.4'
13
13
  # bigdecimal is not part of the default gems starting from Ruby 3.4.0.
@@ -33,15 +33,15 @@ begin
33
33
  # So neither environment can install the other's sqlite3 gem. We
34
34
  # must install sqlite3, but iff we are not building with macOS'
35
35
  # Ruby installation.
36
- dependencies.push('sqlite3')
36
+ dependencies.push('sqlite3')
37
37
  end
38
38
 
39
39
  di = Gem::DependencyInstaller.new(install_dir: gemdir)
40
40
  dependencies.each { |d| di.install(d) }
41
- rescue Exception => e
41
+ rescue Exception
42
42
  exit(1)
43
- end
43
+ end
44
44
 
45
- File.write("Makefile", "clean:\n\ttrue\ninstall:\n\ttrue")
45
+ File.write('Makefile', "clean:\n\ttrue\ninstall:\n\ttrue")
46
46
 
47
47
  exit(0)
data/lib/ToICalPal.rb CHANGED
@@ -76,7 +76,7 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
76
76
  def accept_list_start(_arg)
77
77
  return if @opts[:nb] || @item['placeholder']
78
78
 
79
- if @item['due_date'] && (@item['due_date']).between?(0, $now.to_i)
79
+ if @item['due_date'] && (@item['due_date']).between?(0, $nowto_i)
80
80
  # Use alert bullet for overdue items
81
81
  @res << "#{@opts[:ab]} "
82
82
  else
data/lib/defaults.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # Does anybody really know what time it is?
2
- now = Time.now
3
- $now = ICalPal::RDT.from_time(now)
4
- $today = ICalPal::RDT.new(*$now.to_a[0..2] + [ 0, 0, 0 ])
2
+ $now = Time.now
3
+ $nowto_i = $now.to_i
4
+ $nowrdt = ICalPal::RDT.from_time($now)
5
+ $today = $nowrdt.day_start
5
6
 
6
7
  # Defaults
7
8
  $defaults = {
data/lib/event.rb CHANGED
@@ -1,5 +1,3 @@
1
- r 'timezone'
2
-
3
1
  module ICalPal
4
2
  # Class representing items from the <tt>CalendarItem</tt> table
5
3
  class Event
@@ -13,8 +11,8 @@ module ICalPal
13
11
  def []=(k, v)
14
12
  @self[k] = v
15
13
 
16
- @self['sctime'] = Time.at(@self['sdate'].to_i, in: '+00:00') if k == 'sdate'
17
- @self['ectime'] = Time.at(@self['edate'].to_i, in: '+00:00') if k == 'edate'
14
+ @self['sctime'] = Time.at(@self['sdate'].to_i) if k == 'sdate'
15
+ @self['ectime'] = Time.at(@self['edate'].to_i) if k == 'edate'
18
16
  end
19
17
 
20
18
  # Standard accessor with special handling for +age+,
@@ -50,17 +48,17 @@ module ICalPal
50
48
  (@self['notes'])? @self['notes'].strip.gsub("\n", $opts[:nnr]) : nil
51
49
 
52
50
  when 'sday' # pseudo-property
53
- RDT.new(*@self['sdate'].to_a[0..2])
51
+ @self['sdate'].day_start
54
52
 
55
53
  when 'status' # Integer -> String
56
54
  EventKit::EKEventStatus.select { |_k, v| v == @self['status'] }.keys[0]
57
55
 
58
- when 'event', 'name', 'title' # title[ (age N)]
59
- @self['title'] + ((@self['calendar'] == 'Birthdays')? " (age #{self['age']})" : '')
60
-
61
56
  when 'uid' # for icalBuddy
62
57
  @self['UUID']
63
58
 
59
+ when 'event', 'name', 'title' # title[ (age N)]
60
+ @self['title'] + ((@self['calendar'] == 'Birthdays')? " (age #{self['age']})" : '')
61
+
64
62
  else @self[k]
65
63
  end
66
64
  end
@@ -78,7 +76,7 @@ module ICalPal
78
76
  'sdate' => obj,
79
77
  'placeholder' => true,
80
78
  'title' => 'Nothing.',
81
- } if obj.is_a?(DateTime)
79
+ } if obj.is_a?(DateTime) && $opts[:sed]
82
80
 
83
81
  super
84
82
 
@@ -92,17 +90,14 @@ module ICalPal
92
90
  obj.keys.select { |i| i.end_with? '_date' }.each do |k|
93
91
  next unless obj[k]
94
92
 
95
- begin
96
- zone = Timezone.fetch(obj['start_tz'])
97
- rescue Timezone::Error::InvalidZone
98
- zone = '+00:00'
99
- end
100
-
101
93
  # Save as seconds, Time, RDT
102
94
  ctime = obj[k] + ITIME
95
+ ctime -= $now.utc_offset if obj['start_tz'] == '_float'
96
+ ttime = Time.at(ctime)
97
+
103
98
  @self["#{k[0]}seconds"] = ctime
104
- @self["#{k[0]}ctime"] = Time.at(ctime)
105
- @self["#{k[0]}date"] = RDT.from_time(Time.at(ctime, in: zone))
99
+ @self["#{k[0]}ctime"] = ttime
100
+ @self["#{k[0]}date"] = RDT.from_time(ttime)
106
101
  end
107
102
 
108
103
  @self.delete('unique_identifier')
@@ -122,14 +117,14 @@ module ICalPal
122
117
  return events if nDays > 100_000
123
118
 
124
119
  # If multi-day, each (unique) day needs to end at 23:59:59
125
- self['edate'] = RDT.new(*@self['sdate'].to_a[0..2] + [ 23, 59, 59 ]) if nDays.positive?
120
+ self['edate'] = RDT.new(*@self['sdate'].to_a[0..2] + [ 23, 59, 59 ], @self['sdate'].zone) if nDays.positive?
126
121
 
127
122
  # Repeat for multi-day events
128
123
  (nDays + 1).times do |i|
129
- break if self['sdate'] > $opts[:to]
124
+ break unless $opts[:now] || @self['sdate'] <= $opts[:to]
130
125
 
131
- if in_window?(self['sdate'], self['edate'])
132
- self['daynum'] = i + 1 if nDays.positive?
126
+ if in_window?(@self['sdate'], @self['edate'])
127
+ @self['daynum'] = i + 1 if nDays.positive?
133
128
  events.push(clone)
134
129
  end
135
130
 
@@ -156,7 +151,7 @@ module ICalPal
156
151
  changes = [ { orig_date: -1 } ]
157
152
  changes += $rows.select { |r| r['orig_item_id'] == self['ROWID'] }
158
153
 
159
- while self['sdate'] <= stop
154
+ while @self['sdate'] <= stop
160
155
  # count
161
156
  break if self['count'].positive? && count > self['count']
162
157
 
@@ -240,8 +235,8 @@ module ICalPal
240
235
  m = mo.to_i
241
236
 
242
237
  # Set dates to the first of <m>
243
- nsdate = RDT.new(self['sdate'].year, m, 1, self['sdate'].hour, self['sdate'].minute, self['sdate'].second)
244
- nedate = RDT.new(self['edate'].year, m, 1, self['edate'].hour, self['edate'].minute, self['edate'].second)
238
+ nsdate = RDT.new(@self['sdate'].year, m, 1, @self['sdate'].hour, @self['sdate'].min, @self['sdate'].sec, @self['sdate'].zone)
239
+ nedate = RDT.new(@self['edate'].year, m, 1, @self['edate'].hour, @self['edate'].min, @self['edate'].sec, @self['edate'].zone)
245
240
 
246
241
  # ...but not in the past
247
242
  nsdate >>= 12 if nsdate.month < m
@@ -305,7 +300,7 @@ module ICalPal
305
300
  # @return [Boolean]
306
301
  def in_window?(s, e)
307
302
  if $opts[:now]
308
- if ($now >= s && $now < e)
303
+ if $nowto_i.between?(s.to_i, e.to_i)
309
304
  $log.debug("now: #{s} to #{e} vs. #{$now}")
310
305
  true
311
306
  else
data/lib/icalPal.rb CHANGED
@@ -40,6 +40,8 @@ module ICalPal
40
40
 
41
41
  # Prepare the query
42
42
  stmt = db.prepare(q)
43
+
44
+ # Check for "list" and "all" pseudo-properties
43
45
  abort(stmt.columns.sort.join(' ')) if $opts[:props].any? 'list'
44
46
  $opts[:props] = stmt.columns - $opts[:eep] if $opts[:props].any? 'all'
45
47
 
@@ -50,21 +52,6 @@ module ICalPal
50
52
  # Close the database
51
53
  db.close
52
54
  $log.debug("Closed #{db_file}")
53
-
54
- rescue SQLite3::BusyException => e
55
- $log.error("Non-fatal error closing database #{db.filename}")
56
- raise e
57
-
58
- rescue SQLite3::CantOpenException => e
59
- $log.debug("Can't open #{db_file}")
60
- raise e
61
-
62
- rescue SQLite3::SQLException => e
63
- $log.info("#{db_file}: #{e}")
64
- raise e
65
-
66
- rescue SQLite3::Exception => e
67
- abort("#{db_file}: #{e}")
68
55
  end
69
56
 
70
57
  rows
@@ -88,14 +75,17 @@ module ICalPal
88
75
  obj['symbolic_color_name'] ||= type[:color]
89
76
  end
90
77
 
91
- # Create a new CSV::Row with values from +self+. Newlines are
92
- # replaced with '\n' to ensure each Row is a single line of text.
78
+ # Create a new CSV::Row with values from +self+. Control characters
79
+ # are escaped to ensure they are not interpreted by the terminal.
93
80
  #
94
81
  # @param headers [Array] Key names used as the header row in a CSV::Table
95
82
  # @return [CSV::Row] The +Store+, +Calendar+, +CalendarItem+, or
96
83
  # +Reminder+ as a CSV::Row
97
84
  def to_csv(headers)
98
- values = headers.map { |h| (@self[h].respond_to?(:gsub))? @self[h].gsub("\n", '\n') : @self[h] }
85
+ values = headers.map do |h|
86
+ (@self[h].respond_to?(:gsub))?
87
+ @self[h].gsub(/([[:cntrl:]])/) { |c| c.dump[1..-2] } : @self[h]
88
+ end
99
89
 
100
90
  CSV::Row.new(headers, values)
101
91
  end
@@ -120,7 +110,7 @@ module ICalPal
120
110
  def self.nth(n, dow, m)
121
111
  # Get the number of days in the month by advancing to the first of
122
112
  # the next month, then going back one day
123
- a = [ RDT.new(m.year, m.month, 1, m.hour, m.minute, m.second) ]
113
+ a = [ RDT.new(m.year, m.month, 1, m.hour, m.min, m.sec, m.zone) ]
124
114
  a[1] = (a[0] >> 1) - 1
125
115
 
126
116
  # Reverse it if going backwards
@@ -134,7 +124,7 @@ module ICalPal
134
124
  end
135
125
  end
136
126
 
137
- # Epoch + 31 years
127
+ # Epoch + 31 years (Mon Jan 1 00:00:00 UTC 2001)
138
128
  ITIME = 978_307_200
139
129
 
140
130
  # Days of the week abbreviations used in recurrence rules
data/lib/options.rb CHANGED
@@ -239,7 +239,7 @@ module ICalPal
239
239
 
240
240
  @op.parse!(o, into: cf)
241
241
  rescue StandardError
242
- end unless cli[:norc]
242
+ end unless cli[:norc] && !cli[:cf]
243
243
 
244
244
  # Find command
245
245
  cli[:cmd] ||= @op.default_argv[0]
@@ -254,22 +254,31 @@ module ICalPal
254
254
  cli[:cmd] = cli[:cmd].sub('reminders', 'tasks')
255
255
  cli[:cmd] = cli[:cmd].sub('datedReminders', 'datedTasks')
256
256
 
257
- # Parse eventsNow and eventsToday commands
258
- cli[:cmd].match('events(Now|Today|Remaining)(\+[0-9]+)?') do |m|
259
- cli[:now] = true if m[1] == 'Now'
260
- cli[:days] = (m[1] == 'Today')? m[2].to_i : 1
257
+ # Handle events command variants
258
+ cli[:cmd].match('events(?<v>Now|Today|Remaining)(?<n>\+[0-9]+)?') do |m|
259
+ cli[:cmd] = 'events'
261
260
 
262
- if m[1] == 'Remaining'
263
- cli[:n] = true
264
- cli[:days] = 0
265
- end
261
+ case m.named_captures['v']
262
+ when 'Now'
263
+ cli[:now] = true
266
264
 
267
- cli[:from] = $today
268
- cli[:to] = $today + cli[:days] if cli[:days]
269
- cli[:days] = Integer(cli[:to] - cli[:from])
265
+ when 'Today'
266
+ cli[:from] = $today
267
+ cli[:days] = (m.named_captures['n'])? m.named_captures['n'].to_i : 1
270
268
 
271
- cli[:cmd] = 'events'
272
- end if cli[:cmd]
269
+ when 'Remaining'
270
+ cli[:from] = RDT.from_time($now)
271
+ cli[:to] = $today.day_end
272
+ cli[:days] = 1
273
+ end
274
+ end
275
+
276
+ # Handle tasks command variants
277
+ if cli[:cmd] == 'tasksDueBefore'
278
+ cli.delete(:days) unless cli[:days]
279
+ cli[:from] = RDT.from_epoch(0) unless cli[:from]
280
+ end
281
+ cli[:cmd] = 'tasks' if %w[ datedTasks undatedTasks tasksDueBefore ].include? cli[:cmd]
273
282
 
274
283
  # Must have a valid command
275
284
  raise(OptionParser::InvalidArgument, "Unknown COMMAND #{cli[:cmd]}") unless (COMMANDS.any? cli[:cmd])
@@ -281,13 +290,6 @@ module ICalPal
281
290
  .merge(env)
282
291
  .merge(cli)
283
292
 
284
- # Other tasks commands
285
- if opts[:cmd] == 'tasksDueBefore'
286
- opts.delete(:days) unless opts[:days]
287
- opts[:from] = RDT.from_epoch(0) unless opts[:from]
288
- end
289
- opts[:cmd] = 'tasks' if %w[ datedTasks undatedTasks tasksDueBefore ].include? opts[:cmd]
290
-
291
293
  # Make sure opts[:db] and opts[:tasks] are Arrays
292
294
  opts[:db] = [ opts[:db] ] unless opts[:db].is_a?(Array)
293
295
  opts[:tasks] = [ opts[:tasks] ] unless opts[:db].is_a?(Array)
@@ -310,11 +312,19 @@ module ICalPal
310
312
  opts[:days] -= 1 if opts[:days]
311
313
 
312
314
  if opts[:from]
315
+ # -n
316
+ opts[:from] = RDT.from_time($now) if opts[:n]
317
+
318
+ # Default :to is :from + 1 day
319
+ # --days overrides
313
320
  opts[:to] ||= opts[:from] + 1 if opts[:from]
314
321
  opts[:to] = opts[:from] + opts[:days] if opts[:days]
315
- opts[:to] = RDT.new(*opts[:to].to_a[0..2] + [ 23, 59, 59 ])
322
+
323
+ # Make :to be end of day
324
+ opts[:to] = opts[:to].day_end
325
+
326
+ # Calculate days unless specified
316
327
  opts[:days] ||= Integer(opts[:to] - opts[:from])
317
- opts[:from] = $now if opts[:n]
318
328
  end
319
329
 
320
330
  # Sorting
@@ -337,6 +347,8 @@ module ICalPal
337
347
  raise(OptionParser::InvalidArgument, '--li cannot be negative') if opts[:li].negative?
338
348
  raise(OptionParser::InvalidOption, 'Start date must be before end date') if opts[:from] && opts[:from] > opts[:to]
339
349
  raise(OptionParser::MissingArgument, 'No properties to display') if opts[:props].empty?
350
+ raise(OptionParser::InvalidArgument, 'Cannot use remind output with tasks') if opts[:cmd] == 'tasks' &&
351
+ opts[:output] == 'remind'
340
352
 
341
353
  rescue StandardError => e
342
354
  @op.abort("#{e}\n\n#{@op.help}\n#{e}")
@@ -346,7 +358,9 @@ module ICalPal
346
358
  end
347
359
 
348
360
  # Commands that can be run
349
- COMMANDS = %w[events eventsToday eventsNow eventsRemaining tasks datedTasks undatedTasks tasksDueBefore calendars accounts].freeze
361
+ COMMANDS = %w[events eventsToday eventsNow eventsRemaining
362
+ tasks datedTasks undatedTasks tasksDueBefore
363
+ calendars accounts].freeze
350
364
 
351
365
  # Supported output formats
352
366
  OUTFORMATS = %w[ansi csv default hash html json md rdoc remind toc xml yaml].freeze
data/lib/rdt.rb CHANGED
@@ -4,7 +4,7 @@ module ICalPal
4
4
 
5
5
  # Create a new RDT from a Time object
6
6
  def self.from_time(t)
7
- new(*t.to_a[0..5].reverse)
7
+ new(*t.to_a[0..5].reverse, (t.gmt_offset / 3600).to_s)
8
8
  end
9
9
 
10
10
  # Create a new RDT from seconds since epoch
@@ -42,8 +42,9 @@ module ICalPal
42
42
  # today.
43
43
  def to_s
44
44
  return strftime($opts[:df]) if $opts && $opts[:nrd] && $opts[:df]
45
+ return super unless $today && $opts
45
46
 
46
- case Integer(RDT.new(year, month, day) - $today)
47
+ case Integer(RDT.new(*ymd, month, day) - $today)
47
48
  when -2 then 'day before yesterday'
48
49
  when -1 then 'yesterday'
49
50
  when 0 then 'today'
@@ -67,17 +68,19 @@ module ICalPal
67
68
  to_time.to_i
68
69
  end
69
70
 
70
- # @return [Array] Only the year, month and day of self
71
- def ymd
72
- [ year, month, day ]
71
+ # @return [RDT] Self at 00:00:00
72
+ def day_start
73
+ RDT.new(year, month, day, 0, 0, 0, zone)
73
74
  end
74
75
 
75
- # @see ICalPal::RDT.to_s
76
- #
77
- # @return [Boolean]
78
- def ==(other)
79
- self.to_s == other.to_s
76
+ # @return [RDT] Self at 23:59:59
77
+ def day_end
78
+ RDT.new(year, month, day, 23, 59, 59, zone)
80
79
  end
81
80
 
81
+ # @return [Array] Only the year, month and day of self
82
+ def ymd
83
+ [ year, month, day ]
84
+ end
82
85
  end
83
86
  end
data/lib/reminder.rb CHANGED
@@ -1,4 +1,4 @@
1
- r 'timezone'
1
+ r 'tzinfo'
2
2
 
3
3
  module ICalPal
4
4
  # Class representing items from the <tt>Reminders</tt> database
@@ -8,6 +8,12 @@ module ICalPal
8
8
  def self.load_data(db_file, q)
9
9
  # Load items
10
10
  ICalPal.load_data(db_file, q)
11
+
12
+ rescue SQLite3::SQLException => e
13
+ # Data-local.sqlite does not have zremcdBaseList
14
+ raise e unless e.message =~ /no such table/
15
+
16
+ []
11
17
  end
12
18
 
13
19
  def [](k)
@@ -53,7 +59,7 @@ module ICalPal
53
59
  when 'name', 'reminder', 'task' # Aliases
54
60
  @self['title']
55
61
 
56
- else @self[k]
62
+ else super
57
63
  end
58
64
  end
59
65
 
@@ -91,8 +97,8 @@ module ICalPal
91
97
  if @self['due_date']
92
98
  begin
93
99
  @self['due_date'] += ITIME
94
- zone = Timezone.fetch(@self['timezone'])
95
- rescue Timezone::Error::InvalidZone
100
+ zone = TZInfo::Timezone.get(@self['timezone'])
101
+ rescue TZInfo::InvalidTimezoneIdentifier
96
102
  zone = '+00:00'
97
103
  end
98
104
 
@@ -169,15 +175,12 @@ bl1.zParentList as parent,
169
175
  bl1.zSharingStatus as shared,
170
176
  bl1.zShouldCategorizeGroceryItems as grocery,
171
177
 
172
- -- section members
173
178
  json(bl1.ZMembershipsOfRemindersInSectionsAsData) -> '$.memberships' AS members,
174
179
 
175
- -- group
176
180
  (SELECT zName
177
181
  FROM zremcdBaseList bl2
178
182
  WHERE bl2.z_pk = bl1.zParentList) AS 'group',
179
183
 
180
- -- location
181
184
  (SELECT json_group_array(zremcdObject.zTitle)
182
185
  FROM zremcdObject
183
186
  WHERE zremcdObject.z_pk IN (
@@ -186,7 +189,6 @@ WHERE zremcdObject.z_pk IN (
186
189
  WHERE zremcdObject.zReminder = r1.z_pk
187
190
  )) AS location,
188
191
 
189
- -- proximity
190
192
  (SELECT json_group_array(zremcdObject.zProximity)
191
193
  FROM zremcdObject
192
194
  WHERE zremcdObject.z_pk IN (
@@ -195,7 +197,6 @@ WHERE zremcdObject.z_pk IN (
195
197
  WHERE zremcdObject.zReminder = r1.z_pk
196
198
  )) AS proximity,
197
199
 
198
- -- radius
199
200
  (SELECT json_group_array(zremcdObject.zRadius)
200
201
  FROM zremcdObject
201
202
  WHERE zremcdObject.z_pk IN (
@@ -204,17 +205,15 @@ WHERE zremcdObject.z_pk IN (
204
205
  WHERE zremcdObject.zReminder = r1.z_pk
205
206
  )) AS radius,
206
207
 
207
- -- tags
208
208
  (SELECT json_group_array(zName)
209
209
  FROM zremcdHashtagLabel
210
210
  WHERE zremcdHashtagLabel.z_pk IN (
211
211
  SELECT zremcdObject.zHashtagLabel
212
212
  FROM zremcdObject
213
- JOIN zremcdreminder ON zremcdObject.zReminder3 = r1.z_pk
213
+ JOIN zremcdReminder ON zremcdObject.zReminder3 = r1.z_pk
214
214
  WHERE zremcdObject.zReminder3 = r1.z_pk
215
215
  )) AS tags,
216
216
 
217
- -- assignee
218
217
  (SELECT
219
218
  json_array(zNickname, zFirstName, zLastName, zAddress1)
220
219
  FROM zremcdObject
@@ -224,7 +223,6 @@ WHERE zremcdObject.z_pk IN (
224
223
  WHERE zReminder1 = r1.z_pk
225
224
  )) AS assignee,
226
225
 
227
- -- url
228
226
  (SELECT zURL
229
227
  FROM zremcdObject
230
228
  WHERE zReminder2 = r1.z_pk) AS url
data/lib/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module ICalPal
2
2
  NAME = 'icalPal'.freeze
3
- VERSION = '3.9.1'.freeze
3
+ VERSION = '3.9.3'.freeze
4
4
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icalPal
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.1
4
+ version: 3.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Rosen
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: |
13
13
  Inspired by icalBuddy and maintains close compatability. Includes
@@ -62,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
62
  - !ruby/object:Gem::Version
63
63
  version: '0'
64
64
  requirements: []
65
- rubygems_version: 3.6.6
65
+ rubygems_version: 3.7.2
66
66
  specification_version: 4
67
67
  summary: Command-line tool to query the macOS Calendar and Reminders
68
68
  test_files: []