icalPal 3.2.0 → 3.4.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: cc2cfd940c507b46cafd9c893beb25c47c82850ba7c1e02aaffcb6bc76eda208
4
- data.tar.gz: e28610d838feaf2c05b41badd96bad57185501529cd73c7f0b7f5889290a59ef
3
+ metadata.gz: 3a74d33ac1b8d65466f6ef4820813ad983c7bb539438c11d12eb61daaa2af83f
4
+ data.tar.gz: 3c2790143a7b5abde6304312099988de264cc767fde30c34dd8b7682a80bd782
5
5
  SHA512:
6
- metadata.gz: 2cdd430f7a66418a87cccf5c037f24d0e4fe39e594db5b951ea004c13eadafd15f398bfc0beac8b5fe4c987664a1ee56d9e113354a438e06cf2fe9c3d88c3304
7
- data.tar.gz: ee2276cb96b75d93727bc9dcf4595cd8bef64ccd0893e4e3647f0c5a74d559cddf86434bf0a392176e3a05f7339d8b9651347e5c6098c45467aaf8c163349b62
6
+ metadata.gz: 031fae41c01699e5eb757dd0f838e702fccd0d65fb3ab1aca3a6dde20083ed5ec43c88a1bc2c43e11e0a77399aea21e860d3f13f7590d90a00f912d3d87bff1b
7
+ data.tar.gz: 9e1cfa76f56f735406553d40d13e16ce786461ab331eea9cc0757ffe0858e92ef3c9bae843934f9a55098ec1a28d6e03852aed3ce3bc244a56de362b584de5bd
data/README.md CHANGED
@@ -4,11 +4,27 @@
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 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.
11
11
 
12
+ <!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
13
+
14
+ **Table of Contents**
15
+
16
+ - [Installation](#installation)
17
+ - [Features](#features)
18
+ - [Compatability with icalBuddy](#compatability-with-icalbuddy)
19
+ - [Additional commands](#additional-commands)
20
+ - [Additional options](#additional-options)
21
+ - [Usage](#usage)
22
+ - [Output formats](#output-formats)
23
+ - [History](#history)
24
+
25
+ <!-- markdown-toc end -->
26
+
27
+
12
28
  ## Installation
13
29
 
14
30
  As a system-wide Ruby gem:
@@ -23,18 +39,11 @@ or in your home diretory:
23
39
  gem install --user-install icalPal
24
40
  ```
25
41
 
26
- As a Homebrew formula:
27
-
28
- ```
29
- brew tap ajrosen/icalPal
30
- brew install icalPal
31
- ```
32
-
33
42
  ## Features
34
43
 
35
- ### Compatability with [icalBuddy](https://github.com/ali-rantakari/icalBuddy)
44
+ ### Compatability with icalBuddy
36
45
 
37
- icalPal tries to be compatible with icalBuddy for command-line options
46
+ icalPal tries to be compatible with [icalBuddy](https://github.com/ali-rantakari/icalBuddy) for command-line options
38
47
  and for output. There are a some important differences to be aware
39
48
  of.
40
49
 
@@ -62,7 +71,7 @@ Shows only reminders that have a due date.
62
71
  * The ```-c``` part is optional, but you cannot abbreviate the command if you leave it off.
63
72
  * Use ```-o``` to print the output in different formats. CSV or JSON are intertesting choices.
64
73
  * Copy your Calendar database file and use ```--db``` on it.
65
- * ```--it``` and ```--et``` will filter by Calendar *type*. Types are **Local**, **Exchange**, **CalDAV**, **MobileMe**, **Subscribed**, and **Birthdays**
74
+ * ```--it``` and ```--et``` will filter by Calendar *type*. Types are **Local**, **Exchange**, **CalDAV**, **MobileMe**, **Subscribed**, **Birthdays**, and **Reminders**
66
75
  * ```--il``` and ```-el``` will filter by Reminder list
67
76
  * ```--ia``` includes *only* all-day events (opposite of ```--ea```)
68
77
  * ```--aep``` is like ```--iep```, but *adds* to the default property list instead of replacing it.
@@ -201,6 +210,8 @@ Environment variables:
201
210
  ICALPAL Additional arguments
202
211
  ICALPAL_CONFIG Additional arguments from a file
203
212
  (default: /Users/ajr/.icalpal)
213
+
214
+ Do not quote or escape values. Options set in ICALPAL override ICALPAL_CONFIG. Options on the command line override ICALPAL.
204
215
  ```
205
216
 
206
217
  ## Output formats
@@ -212,7 +223,7 @@ CSV, Hash, JSON, XML, and YAML print all fields for all items in their
212
223
  respective formats. From that you can analyze the results any way you
213
224
  like.
214
225
 
215
- [Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built in icalPal.
226
+ [Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built into icalPal.
216
227
 
217
228
  Other formats such as ANSI, HTML, Markdown, RDoc, and TOC, use Ruby's
218
229
  [RDoc::Markup](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup.html)
data/bin/icalPal CHANGED
@@ -11,6 +11,7 @@ begin
11
11
 
12
12
  require_relative '../lib/icalpal'
13
13
  require_relative '../lib/options'
14
+ require_relative '../lib/utils'
14
15
  rescue LoadError => e
15
16
  dep = e.message[/-- (.*)/, 1]
16
17
 
@@ -28,14 +29,14 @@ end
28
29
  # All kids love log!
29
30
  $log = Logger.new(STDERR, { level: $defaults[:common][:debug] })
30
31
  $log.formatter = proc do |s, t, _p, m| # Severity, time, progname, msg
31
- ($log.level.positive?)? "#{s}: #{m}\n" :
32
- format("[%-5<sev>s] %<time>s [%<caller>s] - %<message>s\n",
33
- {
34
- sev: s,
35
- time: t.strftime('%H:%M:%S.%L'),
36
- caller: caller(4, 1)[0].split('/')[-1],
37
- message: m
38
- })
32
+ format("[%-5<sev>s] %<time>s [%<file>s:%<line>5s] - %<message>s\n",
33
+ {
34
+ sev: s,
35
+ time: t.strftime('%H:%M:%S.%L'),
36
+ file: caller(4, 1)[0].split('/')[-1].split(':')[0],
37
+ line: caller(4, 1)[0].split('/')[-1].split(':')[1],
38
+ message: m
39
+ })
39
40
  end
40
41
 
41
42
  $opts = ICalPal::Options.new.parse_options
@@ -56,9 +57,8 @@ $log.info("Options: #{$opts}")
56
57
  # @param item[Object]
57
58
 
58
59
  def add(item)
59
- $log.info("Adding #{item.inspect} #{item['UUID']} (#{item['title']})") if item['UUID']
60
+ $log.debug("Adding #{item.dump} #{item['UUID']} (#{item['title']})") if item['UUID']
60
61
 
61
- item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if ICalPal::Event === item && item['sdate']
62
62
  $items.push(item)
63
63
  end
64
64
 
@@ -101,114 +101,84 @@ end
101
101
  # Make sure we opened at least one database
102
102
  unless success
103
103
  $log.fatal('Could not open database')
104
- $opts[:db].each { |db| $log.fatal("Tried #{db}") }
104
+
105
+ # SQLite3 does not return useful error messages. If any databases
106
+ # failed because of EPERM (operation not permitted), our parent
107
+ # process might need Full Disk Access, and we should suggest that.
108
+ eperm = 0
109
+
110
+ $opts[:db].each do |db|
111
+ # Use a real open to get a useful error
112
+ File.open(db).close
113
+ rescue Exception => e
114
+ $log.fatal("#{e.class}: #{db}")
115
+
116
+ eperm = 1 if e.instance_of?(Errno::EPERM)
117
+ end
118
+
119
+ if eperm.positive?
120
+ $stderr.puts
121
+ $stderr.puts "Does #{ancestor} have Full Disk Access in System Settings?"
122
+ $stderr.puts
123
+ $stderr.puts "Try running: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles'"
124
+ end
105
125
 
106
126
  abort
107
127
  end
108
128
 
109
129
  $log.info("Loaded #{$rows.count} #{klass} rows")
130
+ $log.info("Window is #{$opts[:from]} to #{$opts[:to]}")
110
131
 
111
132
 
112
133
  ##################################################
113
134
  # Process the data
114
135
 
115
136
  # Add rows
116
- $rows.each_with_index do |row, i|
117
- $log.debug("Row #{i}: #{row['ROWID']}:#{row['UUID']} - #{row['account']}/#{row['calendar']}/#{row['title']}")
118
-
137
+ $rows.each do |row|
119
138
  # --es/--is
120
- if $opts[:es].any? row['account']
121
- $log.debug(':es')
122
- next
123
- end
124
-
125
- unless $opts[:is].empty? || ($opts[:is].any? row['account'])
126
- $log.debug(':is')
127
- next
128
- end
139
+ next if $opts[:es].any? row['account']
140
+ next unless $opts[:is].empty? || ($opts[:is].any? row['account'])
129
141
 
130
142
  # --ec/--ic
131
143
  unless klass == ICalPal::Store || !row['calendar']
132
- if $opts[:ec].any? row['calendar']
133
- $log.debug(':ec')
134
- next
135
- end
136
-
137
- unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
138
- $log.debug(':ic')
139
- next
140
- end
144
+ next if $opts[:ec].any? row['calendar']
145
+ next unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
141
146
  end
142
147
 
143
148
  # Instantiate an item
144
149
  item = klass.new(row)
145
150
 
146
151
  # --et/--it
147
- if $opts[:et].any? item['type']
148
- $log.debug(':et')
149
- next
150
- end
151
-
152
- unless $opts[:it].empty? || ($opts[:it].any? item['type'])
153
- $log.debug(':it')
154
- next
155
- end
152
+ next if $opts[:et].any? item['type']
153
+ next unless $opts[:it].empty? || ($opts[:it].any? item['type'])
156
154
 
157
155
  # --el/--il
158
- if $opts[:el].any? item['list_name']
159
- $log.debug(':el')
160
- next
161
- end
162
-
163
- unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
164
- $log.debug(':il')
165
- next
166
- end
156
+ next if $opts[:el].any? item['list_name']
157
+ next unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
167
158
 
168
159
  # --match
169
160
  if $opts[:match]
170
161
  r = $opts[:match].split('=')
171
162
 
172
163
  if item[r[0]].to_s.respond_to?(:match)
173
- unless item[r[0]].to_s.match(Regexp.new(r[1].to_s, Regexp::IGNORECASE))
174
- $log.debug(':match')
175
- next
176
- end
164
+ next unless item[r[0]].to_s =~ Regexp.new(r[1], Regexp::IGNORECASE)
177
165
  end
178
166
  end
179
167
 
180
168
  if ICalPal::Event === item
181
169
  # Check for all-day and cancelled events
182
- if $opts[:ea] && item['all_day'].positive?
183
- $log.debug(':ea')
184
- next
185
- end
186
-
187
- if $opts[:ia] && !item['all_day'].positive?
188
- $log.debug(':ia')
189
- next
190
- end
191
-
192
- if item['status'] == :canceled
193
- $log.debug(':canceled')
194
- next
195
- end
170
+ next if $opts[:ea] && item['all_day'].positive?
171
+ next if $opts[:ia] && !item['all_day'].positive?
172
+ next if item['status'] == :canceled
196
173
 
197
174
  (item['has_recurrences'].positive?)?
198
- item.recurring.each { |i| add(i) } :
199
- item.non_recurring.each { |i| add(i) }
175
+ item.recurring.each { |j| add(j) } :
176
+ item.non_recurring.each { |j| add(j) }
200
177
  else
201
178
  # Check for dated reminders
202
179
  if ICalPal::Reminder === item
203
- if $opts[:dated] == 1 && item['due_date'].positive?
204
- $log.debug(':undated')
205
- next
206
- end
207
-
208
- if $opts[:dated] == 2 && item['due_date'].zero?
209
- $log.debug(':dated')
210
- next
211
- end
180
+ next if $opts[:dated] == 1 && item['due_date'].positive?
181
+ next if $opts[:dated] == 2 && item['due_date'].zero?
212
182
  end
213
183
 
214
184
  add(item)
@@ -227,11 +197,10 @@ end
227
197
 
228
198
  # Sort the rows
229
199
  begin
230
- $log.info("Sorting/uniqing #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
200
+ $log.info("Sorting #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
231
201
 
232
202
  $items.sort_by! { |i| [ i[$opts[:sep]], i[$opts[:sort]], i['sdate'] ] }
233
203
  $items.reverse! if $opts[:reverse]
234
- $items.uniq!
235
204
  rescue Exception => e
236
205
  $log.info("Sorting failed: #{e}\n")
237
206
  end
@@ -277,7 +246,7 @@ unless mu
277
246
  when 'json' then items.map { |i| i.self }.to_json
278
247
  when 'xml'
279
248
  xml = items.map { |i| "<#{$opts[:cmd].chomp('s')}>#{i.to_xml}</#{$opts[:cmd].chomp('s')}>" }
280
- "<#{$opts[:cmd]}>\n#{xml.join('')}</#{$opts[:cmd]}>"
249
+ "<#{$opts[:cmd]}>\n#{xml.join}</#{$opts[:cmd]}>"
281
250
  when 'yaml' then items.map { |i| i.self }.to_yaml
282
251
  when 'remind' then items.map { |i|
283
252
  "REM #{i['sdate'].strftime('%F AT %R')} " +
@@ -342,14 +311,6 @@ items.each_with_index do |i, j|
342
311
  # First property, value only
343
312
  props << RDoc::Markup::Heading.new(2, value.to_s)
344
313
  end
345
-
346
- # unless k.positive?
347
- # # First property, value only
348
- # props << RDoc::Markup::Heading.new(2, value.to_s)
349
- # else
350
- # props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
351
- # props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless (i['placeholder'])
352
- # end
353
314
  end
354
315
 
355
316
  # Print it
data/bin/icalpal ADDED
@@ -0,0 +1,322 @@
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
+ require_relative '../lib/utils'
15
+ rescue LoadError => e
16
+ dep = e.message[/-- (.*)/, 1]
17
+
18
+ $stderr.puts "FATAL: icalpal is missing a dependency: #{dep}"
19
+ $stderr.puts
20
+ $stderr.puts "Install with 'gem install --user-install #{dep}'"
21
+
22
+ exit
23
+ end
24
+
25
+
26
+ ##################################################
27
+ # Load options
28
+
29
+ # All kids love log!
30
+ $log = Logger.new(STDERR, { level: $defaults[:common][:debug] })
31
+ $log.formatter = proc do |s, t, _p, m| # Severity, time, progname, msg
32
+ format("[%-5<sev>s] %<time>s [%<file>s:%<line>5s] - %<message>s\n",
33
+ {
34
+ sev: s,
35
+ time: t.strftime('%H:%M:%S.%L'),
36
+ file: caller(4, 1)[0].split('/')[-1].split(':')[0],
37
+ line: caller(4, 1)[0].split('/')[-1].split(':')[1],
38
+ message: m
39
+ })
40
+ end
41
+
42
+ $opts = ICalPal::Options.new.parse_options
43
+
44
+ $rows = [] # Rows from the database
45
+ $items = [] # Items to be printed
46
+
47
+
48
+ ##################################################
49
+ # All kids love log!
50
+
51
+ $log.info("Options: #{$opts}")
52
+
53
+
54
+ ##################################################
55
+ # Add an item to the list
56
+ #
57
+ # @param item[Object]
58
+
59
+ def add(item)
60
+ $log.debug("Adding #{item.dump} #{item['UUID']} (#{item['title']})") if item['UUID']
61
+
62
+ $items.push(item)
63
+ end
64
+
65
+
66
+ ##################################################
67
+ # Load the data
68
+
69
+ # What are we getting?
70
+ klass = ICalPal.call($opts[:cmd])
71
+ success = false
72
+
73
+ # Get it
74
+ $opts[:db].each do |db|
75
+ $log.debug("Trying #{db}")
76
+
77
+ if klass == ICalPal::Reminder
78
+ begin
79
+ # Load all .sqlite files
80
+ $log.debug("Loading *.sqlite in #{db}")
81
+ Dir.glob("#{db}/*.sqlite").each do |d|
82
+ $rows += ICalPal.load_data(d, klass::QUERY)
83
+ success = true
84
+
85
+ rescue SQLite3::CantOpenException
86
+ # Non-fatal exception, try the next one
87
+ end
88
+ end
89
+ else
90
+ # Load database
91
+ begin
92
+ $rows += ICalPal.load_data(db, klass::QUERY)
93
+ success = true
94
+
95
+ rescue SQLite3::CantOpenException
96
+ # Non-fatal exception, try the next one
97
+ end
98
+ end
99
+ end
100
+
101
+ # Make sure we opened at least one database
102
+ unless success
103
+ $log.fatal('Could not open database')
104
+
105
+ # SQLite3 does not return useful error messages. If any databases
106
+ # failed because of EPERM (operation not permitted), our parent
107
+ # process might need Full Disk Access, and we should suggest that.
108
+ eperm = 0
109
+
110
+ $opts[:db].each do |db|
111
+ # Use a real open to get a useful error
112
+ File.open(db).close
113
+ rescue Exception => e
114
+ $log.fatal("#{e.class}: #{db}")
115
+
116
+ eperm = 1 if e.instance_of?(Errno::EPERM)
117
+ end
118
+
119
+ if eperm.positive?
120
+ $stderr.puts
121
+ $stderr.puts "Does #{ancestor} have Full Disk Access in System Settings?"
122
+ $stderr.puts
123
+ $stderr.puts "Try running: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles'"
124
+ end
125
+
126
+ abort
127
+ end
128
+
129
+ $log.info("Loaded #{$rows.count} #{klass} rows")
130
+ $log.info("Window is #{$opts[:from]} to #{$opts[:to]}")
131
+
132
+
133
+ ##################################################
134
+ # Process the data
135
+
136
+ # Add rows
137
+ $rows.each do |row|
138
+ # --es/--is
139
+ next if $opts[:es].any? row['account']
140
+ next unless $opts[:is].empty? || ($opts[:is].any? row['account'])
141
+
142
+ # --ec/--ic
143
+ unless klass == ICalPal::Store || !row['calendar']
144
+ next if $opts[:ec].any? row['calendar']
145
+ next unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
146
+ end
147
+
148
+ # Instantiate an item
149
+ item = klass.new(row)
150
+
151
+ # --et/--it
152
+ next if $opts[:et].any? item['type']
153
+ next unless $opts[:it].empty? || ($opts[:it].any? item['type'])
154
+
155
+ # --el/--il
156
+ next if $opts[:el].any? item['list_name']
157
+ next unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
158
+
159
+ # --match
160
+ if $opts[:match]
161
+ r = $opts[:match].split('=')
162
+
163
+ if item[r[0]].to_s.respond_to?(:match)
164
+ next unless item[r[0]].to_s =~ Regexp.new(r[1], Regexp::IGNORECASE)
165
+ end
166
+ end
167
+
168
+ if ICalPal::Event === item
169
+ # Check for all-day and cancelled events
170
+ next if $opts[:ea] && item['all_day'].positive?
171
+ next if $opts[:ia] && !item['all_day'].positive?
172
+ next if item['status'] == :canceled
173
+
174
+ (item['has_recurrences'].positive?)?
175
+ item.recurring.each { |j| add(j) } :
176
+ item.non_recurring.each { |j| add(j) }
177
+ else
178
+ # Check for dated reminders
179
+ if ICalPal::Reminder === item
180
+ next if $opts[:dated] == 1 && item['due_date'].positive?
181
+ next if $opts[:dated] == 2 && item['due_date'].zero?
182
+ end
183
+
184
+ add(item)
185
+ end
186
+ end
187
+
188
+ # Add placeholders for empty days
189
+ if $opts[:sed] && $opts[:sd] && klass == ICalPal::Event
190
+ days = $items.collect { |i| i['sday'] }.uniq.sort
191
+
192
+ $opts[:days].times do |n|
193
+ day = $opts[:from] + n
194
+ $items.push(klass.new(day)) unless days.any? { |i| i.to_s == day.to_s }
195
+ end
196
+ end
197
+
198
+ # Sort the rows
199
+ begin
200
+ $log.info("Sorting #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
201
+
202
+ $items.sort_by! { |i| [ i[$opts[:sep]], i[$opts[:sort]], i['sdate'] ] }
203
+ $items.reverse! if $opts[:reverse]
204
+ rescue Exception => e
205
+ $log.info("Sorting failed: #{e}\n")
206
+ end
207
+
208
+ $log.debug("#{$items.count} items remain")
209
+
210
+ # Configure formatting
211
+ mu = case $opts[:output]
212
+ when 'ansi' then RDoc::Markup::ToAnsi.new
213
+ when 'default' then RDoc::Markup::ToICalPal.new($opts)
214
+ when 'html'
215
+ rdoc = RDoc::Options.new
216
+ rdoc.pipe = true
217
+ rdoc.output_decoration = false
218
+ RDoc::Markup::ToHtml.new(rdoc)
219
+ when 'md' then RDoc::Markup::ToMarkdown.new
220
+ when 'rdoc' then RDoc::Markup::ToRdoc.new
221
+ when 'toc' then RDoc::Markup::ToTableOfContents.new
222
+ end
223
+
224
+
225
+ ##################################################
226
+ # Print the data
227
+
228
+ items = $items[0..$opts[:li] - 1]
229
+
230
+ unless mu
231
+ $log.debug("Output in #{$opts[:output]} format")
232
+
233
+ puts case $opts[:output]
234
+ when 'csv'
235
+ # Get all headers
236
+ headers = []
237
+ items.each { |i| headers += i.keys }
238
+ headers.uniq!
239
+
240
+ # Populate a CSV::Table
241
+ table = CSV::Table.new([], headers: headers)
242
+ items.each { |i| table << i.to_csv(headers) }
243
+
244
+ table
245
+ when 'hash' then items.map { |i| i.self }
246
+ when 'json' then items.map { |i| i.self }.to_json
247
+ when 'xml'
248
+ xml = items.map { |i| "<#{$opts[:cmd].chomp('s')}>#{i.to_xml}</#{$opts[:cmd].chomp('s')}>" }
249
+ "<#{$opts[:cmd]}>\n#{xml.join}</#{$opts[:cmd]}>"
250
+ when 'yaml' then items.map { |i| i.self }.to_yaml
251
+ when 'remind' then items.map { |i|
252
+ "REM #{i['sdate'].strftime('%F AT %R')} " +
253
+ "DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i} " +
254
+ "MSG #{i['title']}"
255
+ }.join("\n")
256
+ else abort "No formatter for #{$opts[:output]}"
257
+ end
258
+
259
+ exit
260
+ end
261
+
262
+ $log.debug("Formatting with #{mu.inspect}")
263
+
264
+ doc = RDoc::Markup::Document.new
265
+ section = nil
266
+
267
+ items.each_with_index do |i, j|
268
+ $log.debug("Print #{j}: #{i.inspect}")
269
+
270
+ # --li
271
+ break if $opts[:li].positive? && j >= $opts[:li]
272
+
273
+ # Use RDoc::Markup::Verbatim to save the item
274
+ v = RDoc::Markup::Verbatim.new
275
+ v.format = i
276
+ doc << v
277
+
278
+ # Sections
279
+ if $opts[:sep] && section != i[$opts[:sep]]
280
+ $log.debug("New section '#{$opts[:sep]}': #{i[$opts[:sep]]}")
281
+
282
+ doc << RDoc::Markup::Raw.new($opts[:sep])
283
+
284
+ doc << RDoc::Markup::BlankLine.new if j.positive?
285
+ doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
286
+ doc << RDoc::Markup::Rule.new(0)
287
+
288
+ section = i[$opts[:sep]]
289
+ end
290
+
291
+ # Item
292
+ props = RDoc::Markup::List.new(:BULLET)
293
+
294
+ # Properties
295
+ $opts[:props].each_with_index do |prop, k|
296
+ value = i[prop]
297
+
298
+ next unless value
299
+ next if Array === value && !value[0]
300
+ next if String === value && value.empty?
301
+
302
+ $log.debug("#{prop}: #{value}")
303
+
304
+ # Use Raw to save the property
305
+ props << RDoc::Markup::Raw.new(prop)
306
+
307
+ if k.positive?
308
+ props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
309
+ props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless (i['placeholder'])
310
+ else
311
+ # First property, value only
312
+ props << RDoc::Markup::Heading.new(2, value.to_s)
313
+ end
314
+ end
315
+
316
+ # Print it
317
+ props << RDoc::Markup::BlankLine.new unless props.empty?
318
+
319
+ doc << props
320
+ end
321
+
322
+ print doc.accept(mu)
data/icalPal.gemspec CHANGED
@@ -15,13 +15,43 @@ EOF
15
15
  s.homepage = "https://github.com/ajrosen/#{s.name}"
16
16
  s.licenses = [ 'GPL-3.0-or-later' ]
17
17
 
18
+ s.metadata = {
19
+ 'bug_tracker_uri' => "https://github.com/ajrosen/#{s.name}/issues",
20
+ 'rubygems_mfa_required' => 'true'
21
+ }
22
+
18
23
  s.files = Dir["#{s.name}.gemspec", 'bin/*', 'lib/*.rb']
19
24
  s.executables = [ "#{s.name}" ]
20
25
  s.extra_rdoc_files = [ 'README.md' ]
21
26
 
22
- s.add_runtime_dependency 'nokogiri-plist', '~> 0.5.0'
23
- s.add_runtime_dependency 'sqlite3', '~> 2'
27
+ # The macOS and Homebrew versions of rubygems have incompatible
28
+ # requirements for sqlite3.
29
+ #
30
+ # macOS comes with version 1.3.13, so it does not need to be added
31
+ # as a dependency, but it cannot install anything newer:
32
+ #
33
+ # requires Ruby version >= 3.0, < 3.4.dev. The current ruby version is 2.6.10.
34
+ #
35
+ # Homebrew's Ruby formula does not come with sqlite3, so it does
36
+ # need to be added as a dependency, but it cannot install version
37
+ # 1.3.13:
38
+ #
39
+ # error: call to undeclared function
40
+ #
41
+ # So we must call add_dependency, but iff we are not building with
42
+ # macOS' Ruby installation.
43
+
44
+ s.add_dependency 'nokogiri-plist', '~> 0.5.0'
45
+ s.add_dependency 'sqlite3', '~> 2.6.0' unless s.rubygems_version == `/usr/bin/gem --version`.strip
46
+ s.add_dependency 'timezone', '>= 0.99', '~> 1.3.0'
24
47
 
25
48
  s.bindir = 'bin'
26
49
  s.required_ruby_version = '>= 2.6.0'
50
+
51
+ s.post_install_message = <<-EOF
52
+
53
+ Note: #{ICalPal::NAME} requires "Full Disk Access" in System Settings to access your calendar.
54
+ Make sure the program that runs #{ICalPal::NAME}, not #{ICalPal::NAME} itself, has these permissions.
55
+
56
+ EOF
27
57
  end