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 +4 -4
- data/README.md +23 -12
- data/bin/icalPal +51 -90
- data/bin/icalpal +322 -0
- data/icalPal.gemspec +32 -2
- data/lib/ToICalPal.rb +24 -27
- data/lib/defaults.rb +8 -6
- data/lib/event.rb +101 -80
- data/lib/icalPal.rb +18 -43
- data/lib/options.rb +52 -9
- data/lib/rdt.rb +22 -4
- data/lib/utils.rb +38 -0
- data/lib/version.rb +1 -1
- metadata +36 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a74d33ac1b8d65466f6ef4820813ad983c7bb539438c11d12eb61daaa2af83f
|
4
|
+
data.tar.gz: 3c2790143a7b5abde6304312099988de264cc767fde30c34dd8b7682a80bd782
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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 **
|
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
|
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
|
-
(
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
184
|
-
|
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 { |
|
199
|
-
item.non_recurring.each { |
|
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
|
-
|
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
|
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
|
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
|
-
|
23
|
-
|
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
|