icalPal 1.2.0 → 2.0.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: a6fac725e88ea9664672d6a52877cb99c2278fe851516316cf51e9e264bb4cd1
4
- data.tar.gz: 7332400d9ee46f3aeebad92c2c2098e90e64b721d1cf455ab74a1e231365686c
3
+ metadata.gz: 7f91434d02dd357bcd02342b2d79418b91e23ec789a6a069a01f2f508685d8ca
4
+ data.tar.gz: 51cfa58c767650d0c7e62213ad4a58dbe9a2d5fc42f17d5ce18c8b65e6a178be
5
5
  SHA512:
6
- metadata.gz: db8c28fdeeba37d4a7b3a10dffc1651fd2c8f1b1ede100eceb00e3ee99e853aa2b797e8421fa9bfaab0579775e0ded38ed7e18909f342a682a24540dc0f589a1
7
- data.tar.gz: dc7aa7cc84b653929904a2eeb33407a576bff18a78fb5b58f38b7ffda9d6cc6b4afd3e267977a32f66f13baf2788b1bff4ca9f43ed037aff9e71179cbf01143d
6
+ metadata.gz: 20b4a10406535baf33986bd6bdf859376a6dfd7055815b2f263d6971d6e8255d5d60fa019c49c4de54ec3a4178e3752136478aafa5937176c80adfdfc36cc8aa
7
+ data.tar.gz: 8a5fbde979186c3347ed6c8c52f3b39b81fc6b303680e1923c0b011583b1c63651dc65b33e4e34a3794b5d01430cb7bd4dd66b2f12510374f13626e438aef079
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  icalPal is a command-line tool to query a macOS Calendar database for
8
8
  accounts, calendars, and events. It can be run on any system with
9
9
  [Ruby](https://www.ruby-lang.org/) and access to a Calendar database
10
- file.
10
+ file, or a Reminders database.
11
11
 
12
12
  ## Installation
13
13
 
@@ -25,7 +25,9 @@ and for output. There are a few differences to be aware of.
25
25
 
26
26
  * Options require two hyphens, except for single-letter options that require one hyphen
27
27
  * *eventsFrom* is not supported. Instead there is *--from*, *--to*, and *--days*
28
- * icalPal does not support the *tasks* commands yet
28
+ * *uncompletedTasks* is simply *tasks*
29
+ * *undatedUncompletedTasks* is simply *undatedTasks*
30
+ * *tasksDueBefore:DATE* is not yet supported
29
31
  * The command can go anywhere; it doesn't have to be the last argument
30
32
  * Property separators are comma-delimited
31
33
 
@@ -35,6 +37,10 @@ and for output. There are a few differences to be aware of.
35
37
 
36
38
  Shows a list of enabled Calendar accounts. Internally they are known as *Stores*; you can run ```icalPal stores``` instead.
37
39
 
40
+ ```icalPal datedTasks```
41
+
42
+ Shows only reminders that have a due date.
43
+
38
44
  ### Additional options
39
45
 
40
46
  * Options can be abbreviated, so long as they are unique. Eg., ```icalPal -c ev --da 3``` is the same as ```icalPal -c events --days 3```.
@@ -42,37 +48,45 @@ Shows a list of enabled Calendar accounts. Internally they are known as *Stores
42
48
  * Use ```-o``` to print the output in different formats. CSV or JSON are intertesting choices.
43
49
  * Copy your Calendar database file and use ```--db``` on it.
44
50
  * ```--it``` and ```--et``` will filter by Calendar *type*. Types are **Local**, **Exchange**, **CalDAV**, **MobileMe**, **Subscribed**, and **Birthdays**
51
+ * ```--il``` and ```-el``` will filter by Reminder list
45
52
  * ```--ia``` includes *only* all-day events (opposite of ```--ea```)
46
53
  * ```--aep``` is like ```--iep```, but *adds* to the default property list instead of replacing it.
47
54
  * ```--sep``` to separate by any property, not just calendar (```--sc```) or date (```--sd```)
48
55
  * ```--color``` uses a wider color palette. Calendar colors are what you have chosen in the Calendar app. Not supported in all terminals, but looks great in [iTerm2](https://iterm2.com/).
49
56
 
50
- Because icalPal is written in Ruby, and not a native Mac application, you can run it just about anywhere. It's been tested with version of Ruby (2.6.10) included with macOS, and does not require any external dependencies.
57
+ Because icalPal is written in Ruby, and not a native Mac application, you can run it just about anywhere. It's been tested with the version of Ruby (2.6.10) included with macOS.
51
58
 
52
59
  ## Usage
53
60
 
54
61
  icalPal: Usage: icalPal [options] [-c] COMMAND
55
62
 
56
63
  COMMAND must be one of the following:
57
-
64
+ ```
58
65
  events Print events
66
+ tasks Print tasks
59
67
  calendars Print calendars
60
68
  accounts Print accounts
61
69
 
62
70
  eventsToday Print events occurring today
63
71
  eventsToday+NUM Print events occurring between today and NUM days into the future
64
72
  eventsNow Print events occurring at present time
73
+ datedTasks Print tasks with a due date
74
+ undatedTasks Print tasks with no due date
75
+ ```
65
76
 
66
77
  Global options:
67
-
78
+ ```
68
79
  -c, --cmd=COMMAND Command to run
69
- --db=DB Use DB file instead of Calendar
70
- --cf=FILE Set config file path (default: $HOME/.icalPal)
80
+ --db=DB Use DB file instead of Calendar (default: /Users/ajr/Library/Calendars/Calendar.sqlitedb)
81
+ For the tasks commands this should be a directory containing .sqlite files
82
+ (default: /Users/ajr/Library/Group Containers/group.com.apple.reminders/Container_v1/Stores)
83
+ --cf=FILE Set config file path (default: /Users/ajr/.icalPal)
71
84
  -o, --output=FORMAT Print as FORMAT (default: default)
72
- [ansi, csv, default, hash, html, json, md, rdoc, toc, yaml, remind]
73
-
74
- Including/excluding calendars:
85
+ [ansi, csv, default, hash, html, json, md, rdoc, remind, toc, xml, yaml]
86
+ ```
75
87
 
88
+ Including/excluding calendars and reminders:
89
+ ```
76
90
  --is=ACCOUNTS List of accounts to include
77
91
  --es=ACCOUNTS List of accounts to exclude
78
92
 
@@ -83,8 +97,12 @@ Including/excluding calendars:
83
97
  --ic=CALENDARS List of calendars to include
84
98
  --ec=CALENDARS List of calendars to exclude
85
99
 
86
- Choosing dates:
100
+ --il=LISTS List of reminder lists to include
101
+ --el=LISTS List of reminder lists to exclude
102
+ ```
87
103
 
104
+ Choosing dates:
105
+ ```
88
106
  --from=DATE List events starting on or after DATE
89
107
  --to=DATE List events starting on or before DATE
90
108
  DATE can be yesterday, today, tomorrow, +N, -N, or anything accepted by DateTime.parse()
@@ -95,13 +113,19 @@ Choosing dates:
95
113
  --sed Show empty dates with --sd
96
114
  --ia Include only all-day events
97
115
  --ea Exclude all-day events
116
+ ```
98
117
 
99
118
  Choose properties to include in the output:
100
-
119
+ ```
101
120
  --iep=PROPERTIES List of properties to include
102
121
  --eep=PROPERTIES List of properties to exclude
103
122
  --aep=PROPERTIES List of properties to include in addition to the default list
104
123
 
124
+ --itp=PROPERTIES List of task properties to include
125
+ --etp=PROPERTIES List of task properties to exclude
126
+ --atp=PROPERTIES List of task properties to include in addition to the default list
127
+ Included for backwards compatability, these are aliases for --iep, --eep, and --aep
128
+
105
129
  --uid Show event UIDs
106
130
  --eed Exclude end datetimes
107
131
 
@@ -113,16 +137,20 @@ Choose properties to include in the output:
113
137
 
114
138
  Use 'all' for PROPERTIES to include all available properties (except any listed in --eep)
115
139
  Use 'list' for PROPERTIES to list all available properties and exit
140
+ ```
116
141
 
117
142
  Formatting the output:
118
-
143
+ ```
119
144
  --li=N Show at most N items (default: 0 for no limit)
120
145
 
121
146
  --sc Separate by calendar
122
147
  --sd Separate by date
148
+ --sp Separate by priority
123
149
  --sep=PROPERTY Separate by PROPERTY
124
150
 
125
151
  --sort=PROPERTY Sort by PROPERTY
152
+ --std Sort tasks by due date (same as --sort=due_date)
153
+ --stda Sort tasks by due date (ascending) (same as --sort=due_date -r)
126
154
  -r, --reverse Sort in reverse
127
155
 
128
156
  --ps=SEPARATORS List of property separators
@@ -133,24 +161,28 @@ Formatting the output:
133
161
  See https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html#method-i-strftime for details
134
162
 
135
163
  -b, --bullet=STRING Use STRING for bullets
164
+ --ab=STRING Use STRING for alert bullets
165
+ --nb Do not use bullets
136
166
  --nnr=SEPARATOR Set replacement for newlines within notes
137
167
 
138
168
  -f Format output using standard ANSI colors
139
169
  --color Format output using a larger color palette
170
+ ```
140
171
 
141
172
  Help:
142
-
173
+ ```
143
174
  -h, --help Show this message
144
- -V, -v, --version Show version and exit (1.0)
175
+ -V, -v, --version Show version and exit (2.0.0)
145
176
  -d, --debug=LEVEL Set the logging level (default: warn)
146
177
  [debug, info, warn, error, fatal]
178
+ ```
147
179
 
148
180
  Environment variables:
149
-
181
+ ```
150
182
  ICALPAL Additional arguments
151
183
  ICALPAL_CONFIG Additional arguments from a file
152
- (default: $HOME/.icalPal)
153
-
184
+ (default: /Users/ajr/.icalPal)
185
+ ```
154
186
 
155
187
  ## History
156
188
 
@@ -178,8 +210,9 @@ directly from the Calendar database file instead of an API, you *can*.
178
210
  icalPal supports several output formats. The **default** format tries
179
211
  to mimic icalBuddy as much as possible.
180
212
 
181
- CSV, Hash, JSON, and YAML print all fields for all items in their
182
- respective formats. From that you can analyze the results any way you like.
213
+ CSV, Hash, JSON, XML, and YAML print all fields for all items in their
214
+ respective formats. From that you can analyze the results any way you
215
+ like.
183
216
 
184
217
  [Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built in icalPal.
185
218
 
data/bin/icalPal CHANGED
@@ -44,7 +44,7 @@ $log.info("Options: #{$opts}")
44
44
  def add(item)
45
45
  $log.info("Adding #{item.inspect} #{item['UUID']} (#{item['title']})") if item['UUID']
46
46
 
47
- item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if item['sdate']
47
+ item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if item === ICalPal::Event && item['sdate']
48
48
  $items.push(item)
49
49
  end
50
50
 
@@ -54,54 +54,108 @@ end
54
54
 
55
55
  # What are we getting?
56
56
  klass = ICalPal::($opts[:cmd])
57
- q = klass::QUERY
58
-
59
- $log.debug(q.gsub(/\n/, ' '))
60
57
 
61
58
  # Get it
62
- stmt = $db.prepare(q)
63
- abort(stmt.columns.sort.join(' ')) if $opts[:props].any? 'list'
64
- $opts[:props] = stmt.columns - $opts[:eep] if $opts[:props].any? 'all'
65
-
66
- # Iterate the SQLite3::ResultSet once
67
- stmt.execute.each_with_index { |i, j| $rows[j] = i }
68
- stmt.close
59
+ if klass == ICalPal::Reminder then
60
+ # Load all .sqlite files
61
+ Dir.glob("#{$opts[:db]}/*.sqlite").each { |db| $rows += ICalPal.load_data(db, klass::QUERY) }
62
+ else
63
+ # Load database
64
+ $rows += ICalPal.load_data($opts[:db], klass::QUERY)
65
+ end
69
66
 
70
- $log.info("Loaded #{$rows.count} rows from #{$db.filename}")
71
- $db.close
67
+ $log.info("Loaded #{$rows.count} #{klass} rows")
72
68
 
73
69
 
74
70
  ##################################################
75
71
  # Process the data
76
72
 
77
73
  # Add rows
78
- $rows.each do |row|
74
+ $rows.each_with_index do |row, i|
75
+ $log.debug("Row #{i}: #{row['ROWID']}:#{row['UUID']} - #{row['account']}/#{row['calendar']}/#{row['title']}")
76
+
79
77
  # --es/--is
80
- next if $opts[:es].any? row['account']
81
- next unless $opts[:is].empty? or $opts[:is].any? row['account']
78
+ if $opts[:es].any? row['account'] then
79
+ $log.debug(":es")
80
+ next
81
+ end
82
+
83
+ unless $opts[:is].empty? or $opts[:is].any? row['account']
84
+ $log.debug(":is");
85
+ next
86
+ end
82
87
 
83
88
  # --ec/--ic
84
- next if $opts[:ec].any? row['calendar']
85
- next unless $opts[:ic].empty? or $opts[:ic].any? row['calendar']
89
+ if $opts[:ec].any? row['calendar'] then
90
+ $log.debug(":ec")
91
+ next
92
+ end
93
+
94
+ unless $opts[:ic].empty? or $opts[:ic].any? row['calendar']
95
+ $log.debug(":ic")
96
+ next
97
+ end
86
98
 
99
+ # Instantiate an item
87
100
  item = klass.new(row)
88
101
 
89
102
  # --et/--it
90
- next if $opts[:et].any? item['type']
91
- next unless $opts[:it].empty? or $opts[:it].any? item['type']
103
+ if $opts[:et].any? item['type'] then
104
+ $log.debug(":et")
105
+ next
106
+ end
92
107
 
93
- unless ICalPal::Event === item
94
- # Always add non-event items
95
- add(item)
96
- else
108
+ unless $opts[:it].empty? or $opts[:it].any? item['type']
109
+ $log.debug(":it")
110
+ next
111
+ end
112
+
113
+ # --el/--il
114
+ if $opts[:el].any? item['list_name'] then
115
+ $log.debug(":el")
116
+ next
117
+ end
118
+
119
+ unless $opts[:il].empty? or $opts[:il].any? item['list_name']
120
+ $log.debug(":il")
121
+ next
122
+ end
123
+
124
+ if ICalPal::Event === item
97
125
  # Check for all-day and cancelled events
98
- next if $opts[:ea] && item['all_day'].positive?
99
- next if $opts[:ia] && !item['all_day'].positive?
100
- next if item['status'] == :canceled
126
+ if $opts[:ea] && item['all_day'].positive? then
127
+ $log.debug(":ea")
128
+ next
129
+ end
130
+
131
+ if $opts[:ia] && !item['all_day'].positive? then
132
+ $log.debug(":ia")
133
+ next
134
+ end
135
+
136
+ if item['status'] == :canceled then
137
+ $log.debug(":canceled")
138
+ next
139
+ end
101
140
 
102
141
  (item['has_recurrences'].positive?)?
103
142
  item.recurring.each { |i| add(i) } :
104
143
  item.non_recurring.each { |i| add(i) }
144
+ else
145
+ # Check for dated reminders
146
+ if ICalPal::Reminder === item then
147
+ if $opts[:dated] == 1 and item['due_date'] > 0 then
148
+ $log.debug(":undated")
149
+ next
150
+ end
151
+
152
+ if $opts[:dated] == 2 and item['due_date'] == 0 then
153
+ $log.debug(":dated")
154
+ next
155
+ end
156
+ end
157
+
158
+ add(item)
105
159
  end
106
160
  end
107
161
 
@@ -111,19 +165,23 @@ if $opts[:sed] && $opts[:sd] && klass == ICalPal::Event
111
165
 
112
166
  $opts[:days].times do |n|
113
167
  day = $opts[:from] + n
114
- $items.push(klass.new(day)) unless days.any? { |i| i == day }
168
+ $items.push(klass.new(day)) unless days.any? { |i| i.to_s == day.to_s }
115
169
  end
116
170
  end
117
171
 
118
172
  # Sort the rows
119
173
  begin
174
+ $log.info("Sorting/uniqing #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
175
+
120
176
  $items.sort_by! { |i| [ i[$opts[:sep]], i[$opts[:sort]], i['sdate'] ] }
121
177
  $items.reverse! if $opts[:reverse]
122
178
  $items.uniq!
123
- rescue ArgumentError => e
124
- $log.warn("Sorting failed, results may be unexpected\n")
179
+ rescue Exception => e
180
+ $log.info("Sorting failed: #{e}\n")
125
181
  end
126
182
 
183
+ $log.debug("#{$items.count} items remain")
184
+
127
185
  # Configure formatting
128
186
  mu = case $opts[:output]
129
187
  when 'ansi' then RDoc::Markup::ToAnsi.new
@@ -145,17 +203,25 @@ mu = case $opts[:output]
145
203
  items = $items[0..$opts[:li] - 1]
146
204
 
147
205
  unless mu
206
+ $log.debug("Output in #{$opts[:output]} format")
207
+
148
208
  puts case $opts[:output]
149
209
  when 'csv' then
150
- o = {
151
- headers: items[0].keys,
152
- write_converters: proc { |f| f.respond_to?(:gsub)? f.gsub(/\n/, '\n') : f },
153
- write_headers: true,
154
- }
210
+ # Get all headers
211
+ headers = []
212
+ items.each { |i| headers += i.keys }
213
+ headers.uniq!
214
+
215
+ # Populate a CSV::Table
216
+ table = CSV::Table.new([], headers: headers)
217
+ items.each { |i| table << i.to_csv(headers) }
155
218
 
156
- CSV.generate(**o) { |k| items.each { |i| k << i.values.map { |v| v.to_s } } }
219
+ table
157
220
  when 'hash' then items.map { |i| i.self }
158
221
  when 'json' then items.map { |i| i.self }.to_json
222
+ when 'xml' then
223
+ xml = items.map { |i| "<#{$opts[:cmd].chomp("s")}>#{i.to_xml}</#{$opts[:cmd].chomp("s")}>" }
224
+ "<#{$opts[:cmd]}>\n#{xml.join("")}</#{$opts[:cmd]}>"
159
225
  when 'yaml' then items.map { |i| i.self }.to_yaml
160
226
  when 'remind' then items.map { |i|
161
227
  "REM #{i['sdate'].strftime('%F AT %R')} " +
@@ -168,21 +234,27 @@ unless mu
168
234
  exit
169
235
  end
170
236
 
171
- section = nil if $opts[:sep]
237
+ $log.debug("Formatting with #{mu.inspect}")
238
+
239
+ doc = RDoc::Markup::Document.new
240
+ section = nil
172
241
 
173
242
  items.each_with_index do |i, j|
243
+ $log.debug("Print #{j}: #{i.inspect}")
244
+
174
245
  # --li
175
246
  break if $opts[:li].positive? && j >= $opts[:li]
176
247
 
177
- doc = RDoc::Markup::Document.new
248
+ # Use RDoc::Markup::Verbatim to save the item
249
+ v = RDoc::Markup::Verbatim.new
250
+ v.format = i
251
+ doc << v
178
252
 
179
253
  # Sections
180
254
  if $opts[:sep] && section != i[$opts[:sep]]
181
- $log.debug("Section: #{i[$opts[:sep]]}")
255
+ $log.debug("New section '#{$opts[:sep]}': #{i[$opts[:sep]]}")
182
256
 
183
- v = RDoc::Markup::Verbatim.new
184
- v.format = { item: i, prop: $opts[:sep] }
185
- doc << v
257
+ doc << RDoc::Markup::Raw.new($opts[:sep])
186
258
 
187
259
  doc << RDoc::Markup::BlankLine.new if j.positive?
188
260
  doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
@@ -196,27 +268,30 @@ items.each_with_index do |i, j|
196
268
 
197
269
  # Properties
198
270
  $opts[:props].each_with_index do |prop, k|
199
- next unless i[prop]
200
- next if Array === i[prop] && !i[prop][0]
271
+ value = i[prop]
201
272
 
202
- $log.debug("#{prop}: #{i[prop]}")
273
+ next unless value
274
+ next if Array === value && !value[0]
275
+ next if String === value && value.length == 0
203
276
 
204
- v = RDoc::Markup::Verbatim.new
205
- v.format = { item: i, prop: prop }
206
- props << v
277
+ $log.debug("#{prop}: #{value}")
278
+
279
+ # Use Raw to save the property
280
+ props << RDoc::Markup::Raw.new(prop)
207
281
 
208
282
  unless k.positive?
209
283
  # First property, value only
210
- props << RDoc::Markup::Heading.new(2, i[prop].to_s)
284
+ props << RDoc::Markup::Heading.new(2, value.to_s)
211
285
  else
212
286
  props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
213
- props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(i[prop]))
287
+ props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless(i['placeholder'])
214
288
  end
215
289
  end
216
290
 
217
291
  # Print it
218
- unless props.empty?
219
- doc << props
220
- puts doc.accept(mu)
221
- end
292
+ props << RDoc::Markup::BlankLine.new unless props.empty?
293
+
294
+ doc << props
222
295
  end
296
+
297
+ print doc.accept(mu)
data/icalPal.gemspec CHANGED
@@ -1,6 +1,8 @@
1
+ require './lib/version'
2
+
1
3
  Gem::Specification.new do |s|
2
4
  s.name = "icalPal"
3
- s.version = "1.2.0"
5
+ s.version = ICalPal::VERSION
4
6
  s.summary = "Command-line tool to query the macOS Calendar"
5
7
  s.description = <<-EOF
6
8
  Inspired by icalBuddy and maintains close compatability. Includes
@@ -17,6 +19,7 @@ EOF
17
19
  s.extra_rdoc_files = [ "README.md" ]
18
20
 
19
21
  s.add_runtime_dependency "sqlite3", "~> 1"
22
+ s.add_runtime_dependency "nokogiri-plist", "~> 0.5.0"
20
23
 
21
24
  s.bindir = 'bin'
22
25
  s.required_ruby_version = '>= 2.6.0'
data/lib/EventKit.rb CHANGED
@@ -27,6 +27,13 @@ class EventKit
27
27
  'yearly',
28
28
  ]
29
29
 
30
+ EKReminderProperty = [
31
+ 'none', # 0
32
+ 'high', nil, nil, nil, # 1
33
+ 'medium', nil, nil, nil, # 5
34
+ 'low', # 9
35
+ ]
36
+
30
37
  # EKSourceType (with color)
31
38
  EKSourceType = [
32
39
  { name: 'Local', color: '#FFFFFF' }, # White
data/lib/ToICalPal.rb CHANGED
@@ -7,15 +7,25 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
7
7
  # ANSI[https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-T.416-199303-I!!PDF-E&type=items]
8
8
  # colors
9
9
  ANSI = {
10
- 'black': 30, '#000000': '38;5;0',
11
- 'red': 31, '#ff0000': '38;5;1',
12
- 'green': 32, '#00ff00': '38;5;2',
13
- 'yellow': 33, '#ffff00': '38;5;3',
14
- 'blue': 34, '#0000ff': '38;5;4',
15
- 'magenta': 35, '#ff00ff': '38;5;5',
16
- 'cyan': 36, '#00ffff': '38;5;6',
17
- 'white': 37, '#ffffff': '38;5;255',
18
- 'default': 39, 'custom': nil,
10
+ 'black': 30, '#000000': '38;5;0',
11
+ 'red': 31, '#ff0000': '38;5;1',
12
+ 'green': 32, '#00ff00': '38;5;2',
13
+ 'yellow': 33, '#ffff00': '38;5;3',
14
+ 'blue': 34, '#0000ff': '38;5;4',
15
+ 'magenta': 35, '#ff00ff': '38;5;5',
16
+ 'cyan': 36, '#00ffff': '38;5;6',
17
+ 'white': 37, '#ffffff': '38;5;255',
18
+ 'default': 39, 'custom': nil,
19
+
20
+ # Reminders custom colors
21
+ 'brown': '38;2;162;132;94',
22
+ 'gray': '38;2;91;98;106',
23
+ 'indigo': '38;2;88;86;214',
24
+ 'lightblue': '38;2;90;200;250',
25
+ 'orange': '38;2;255;149;0',
26
+ 'pink': '38;2;255;45;85',
27
+ 'purple': '38;2;204;115;225',
28
+ 'rose': '38;2;217;166;159',
19
29
  }
20
30
 
21
31
  # Increased intensity
@@ -38,6 +48,8 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
38
48
 
39
49
  # @param opts [Hash] Used for conditional formatting
40
50
  # @option opts [String] :bullet Bullet
51
+ # @option opts [String] :ab Alert bullet
52
+ # @option opts [Boolean] :nb No bullet
41
53
  # @option opts [Boolean] :nc No calendar names
42
54
  # @option opts [Boolean] :npn No property names
43
55
  # @option opts [Integer] :palette (nil) 8 for \-f, 24 for \--color
@@ -67,6 +79,14 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
67
79
  rescue
68
80
  end
69
81
 
82
+ begin
83
+ if (@item['due_date'] + ICalPal::ITIME).between?(ICalPal::ITIME + 1, $now.to_i) then
84
+ @res << "#{@opts[:ab]} " unless @opts[:nb]
85
+ return
86
+ end
87
+ rescue
88
+ end
89
+
70
90
  @res << "#{@opts[:bullet]} " unless @opts[:nb]
71
91
  end
72
92
 
@@ -126,15 +146,21 @@ class RDoc::Markup::ToICalPal < RDoc::Markup::Formatter
126
146
  accept_blank_line
127
147
  end
128
148
 
129
- # Don't add anything to the document, just save the item and
130
- # property name for later
149
+ # Don't add anything to the document, just save the item for later
150
+ #
151
+ # @param arg [RDoc::Markup::Verbatim]
152
+ # @option arg [Object] :format The item
153
+ def accept_verbatim(arg)
154
+ @item = arg.format
155
+ end
156
+
157
+ # Don't add anything to the document, just save the property name
158
+ # for later
131
159
  #
132
- # @param h [RDoc::Markup::Verbatim]
133
- # @option h [String] :parts Ignored
134
- # @option h [{item, prop => ICalPal::Event, String}] :format
135
- def accept_verbatim(h)
136
- @item = h.format[:item]
137
- @prop = h.format[:prop]
160
+ # @param arg [RDoc::Markup::Raw]
161
+ # @option arg [Object] :parts The property
162
+ def accept_raw(arg)
163
+ @prop = arg.parts[0]
138
164
  end
139
165
 
140
166
  # @param str [String]
data/lib/defaults.rb CHANGED
@@ -5,17 +5,21 @@ $today = ICalPal::RDT.new(*$now.to_a[0..2] + [0, 0, 0, $now.zone])
5
5
  # Defaults
6
6
  $defaults = {
7
7
  common: {
8
+ ab: '!',
8
9
  aep: [],
9
10
  bullet: '•',
10
11
  cf: "#{ENV['HOME']}/.icalPal",
11
12
  color: false,
12
13
  db: "#{ENV['HOME']}/Library/Calendars/Calendar.sqlitedb",
13
14
  debug: Logger::WARN,
15
+ df: '%b %-d, %Y',
14
16
  ec: [],
15
17
  eep: [],
18
+ el: [],
16
19
  es: [],
17
20
  et: [],
18
21
  ic: [],
22
+ il: [],
19
23
  is: [],
20
24
  it: [],
21
25
  li: 0,
@@ -27,11 +31,25 @@ $defaults = {
27
31
  sep: false,
28
32
  sort: nil,
29
33
  sp: false,
34
+ tf: '%-I:%M %p',
30
35
  },
31
36
  tasks: {
32
- bullet: '!',
33
- iep: [ 'notes', 'due', 'priority' ],
34
- sort: 'priority',
37
+ dated: 0,
38
+ db: ICalPal::Reminder::DB_PATH,
39
+ iep: [ 'title', 'notes', 'due', 'priority' ],
40
+ sort: 'prio',
41
+ },
42
+ undatedTasks: {
43
+ dated: 1,
44
+ db: ICalPal::Reminder::DB_PATH,
45
+ iep: [ 'title', 'notes', 'due', 'priority' ],
46
+ sort: 'prio',
47
+ },
48
+ datedTasks: {
49
+ dated: 2,
50
+ db: ICalPal::Reminder::DB_PATH,
51
+ iep: [ 'title', 'notes', 'due', 'priority' ],
52
+ sort: 'prio',
35
53
  },
36
54
  stores: {
37
55
  iep: [ 'account', 'type' ],
@@ -43,7 +61,6 @@ $defaults = {
43
61
  },
44
62
  events: {
45
63
  days: nil,
46
- df: '%b %-d, %Y',
47
64
  ea: false,
48
65
  eed: false,
49
66
  eep: [],
@@ -57,7 +74,6 @@ $defaults = {
57
74
  sed: false,
58
75
  sort: 'sdate',
59
76
  ss: "\n------------------------",
60
- tf: '%-I:%M %p',
61
77
  to: nil,
62
78
  uid: false,
63
79
  }
data/lib/event.rb CHANGED
@@ -3,6 +3,11 @@ module ICalPal
3
3
  class Event
4
4
  include ICalPal
5
5
 
6
+ # Standard accessor with special handling for +sdate+. Setting
7
+ # +sdate+ will also set +sday+.
8
+ #
9
+ # @param k [String] Key/property name
10
+ # @param v [Object] Key/property value
6
11
  def []=(k, v)
7
12
  @self[k] = v
8
13
  @self['sday'] = ICalPal::RDT.new(*self['sdate'].to_a[0..2]) if k == 'sdate'
@@ -27,7 +32,7 @@ module ICalPal
27
32
  t += ' at ' unless @self['all_day'].positive?
28
33
  end
29
34
 
30
- unless @self['all_day'] && @self['all_day'].positive?
35
+ unless @self['all_day'] && @self['all_day'].positive? || @self['placeholder']
31
36
  t ||= ''
32
37
  t += "#{@self['sdate'].strftime($opts[:tf])}" if @self['sdate']
33
38
  t += " - #{@self['edate'].strftime($opts[:tf])}" unless $opts[:eed] || !@self['edate']
@@ -65,10 +70,11 @@ module ICalPal
65
70
  def initialize(obj)
66
71
  # Placeholder for days with no events
67
72
  return @self = {
68
- $opts[:sep] => obj,
69
- 'placeholder' => true,
70
- 'title' => 'Nothing.',
71
- } if DateTime === obj
73
+ $opts[:sep] => obj,
74
+ 'sdate' => obj,
75
+ 'placeholder' => true,
76
+ 'title' => 'Nothing.',
77
+ } if DateTime === obj
72
78
 
73
79
  @self = {}
74
80
  obj.keys.each { |k| @self[k] = obj[k] }
@@ -108,6 +114,7 @@ module ICalPal
108
114
 
109
115
  # Repeat for multi-day events
110
116
  ((self['duration'] / 86400).to_i + 1).times do |i|
117
+ $log.debug("multi-day event #{i + 1}") if (i > 0)
111
118
  self['daynum'] = i + 1
112
119
  retval.push(clone) if in_window?(self['sdate'])
113
120
  self['sdate'] += 1
@@ -126,14 +133,20 @@ module ICalPal
126
133
 
127
134
  # See if event ends before we start
128
135
  stop = [ $opts[:to], (self['rdate'] || $opts[:to]) ].min
129
- return(retval) if stop < $opts[:from]
136
+ if stop < $opts[:from] then
137
+ $log.debug("#{stop} < #{$opts[:from]}")
138
+ return(retval)
139
+ end
130
140
 
131
141
  # Get changes to series
132
142
  changes = $rows.select { |r| r['orig_item_id'] == self['ROWID'] }
133
143
 
134
144
  i = 1
135
145
  while self['sdate'] <= stop
136
- return(retval) if self['count'].positive? && i > self['count']
146
+ if self['count'].positive? && i > self['count'] then
147
+ $log.debug("count exceeded: #{i} > #{self['count']}")
148
+ return(retval)
149
+ end
137
150
  i += 1
138
151
 
139
152
  unless @self['xdate'].any?(@self['sdate']) # Exceptions?
@@ -179,6 +192,9 @@ module ICalPal
179
192
  when 'O' then ndate = RDT.new(ndate.year, j[1].to_i, ndate.day)
180
193
  when 'S' then @self['specifier'].sub!(/D=0/, "D=+#{j[1].to_i}")
181
194
  end
195
+
196
+ # No time travel!
197
+ ndate = self['sdate'] if ndate <= self['sdate']
182
198
  end
183
199
 
184
200
  # D=Day of the week
@@ -236,9 +252,23 @@ module ICalPal
236
252
  # @param e [RDT] Event end
237
253
  # @return [Boolean]
238
254
  def in_window?(s, e = s)
239
- $opts[:n]?
240
- ($now >= s && $now < e) :
241
- ([ s, e ].max >= $opts[:from] && s < $opts[:to])
255
+ if $opts[:n] then
256
+ if ($now >= s && $now < e) then
257
+ $log.debug("now: #{s} to #{e} vs. #{$now}")
258
+ return(true)
259
+ else
260
+ $log.debug("not now: #{s} to #{e} vs. #{$now}")
261
+ return(false)
262
+ end
263
+ else
264
+ if ([ s, e ].max >= $opts[:from] && s < $opts[:to]) then
265
+ $log.debug("in window: #{s} to #{e} vs. #{$opts[:from]} to #{$opts[:to]}")
266
+ return(true)
267
+ else
268
+ $log.debug("not in window: #{s} to #{e} vs. #{$opts[:from]} to #{$opts[:to]}")
269
+ return(false)
270
+ end
271
+ end
242
272
  end
243
273
 
244
274
  QUERY = <<~SQL
data/lib/icalPal.rb CHANGED
@@ -3,10 +3,12 @@ require_relative 'ToICalPal'
3
3
  require_relative 'calendar'
4
4
  require_relative 'event'
5
5
  require_relative 'rdt'
6
+ require_relative 'reminder'
6
7
  require_relative 'store'
7
8
 
8
9
  # Encapsulate the _Store_ (accounts), _Calendar_ and _CalendarItem_
9
- # tables of a Calendar database
10
+ # tables of a Calendar database, and the _Reminder_ table of a
11
+ # Reminders database
10
12
 
11
13
  module ICalPal
12
14
  attr_reader :self
@@ -14,7 +16,7 @@ module ICalPal
14
16
  # Dynamic instantiation of our classes based on the command being
15
17
  # run
16
18
  #
17
- # @param klass [String] One of +accounts+, +stores+, +calendars+, or +events+
19
+ # @param klass [String] One of +accounts+, +stores+, +calendars+, +events+, or +tasks+
18
20
  # @return [Class] The subclass of ICalPal
19
21
  def self.call(klass)
20
22
  case klass
@@ -22,13 +24,48 @@ module ICalPal
22
24
  when 'stores' then Store
23
25
  when 'calendars' then Calendar
24
26
  when 'events' then Event
25
- when 'tasks' then Task
27
+ when 'tasks' then Reminder
26
28
  else
27
29
  $log.fatal("Unknown class: #{klass}")
28
30
  exit
29
31
  end
30
32
  end
31
33
 
34
+ # Load data
35
+ def self.load_data(db_file, q)
36
+ $log.debug(q.gsub(/\n/, ' '))
37
+
38
+ rows = []
39
+
40
+ begin
41
+ # Open the database
42
+ $log.debug("Opening database: #{db_file}")
43
+ db = SQLite3::Database.new(db_file, { readonly: true, results_as_hash: true })
44
+
45
+ # Prepare the query
46
+ stmt = db.prepare(q)
47
+ abort(stmt.columns.sort.join(' ')) if $opts[:props].any? 'list'
48
+ $opts[:props] = stmt.columns - $opts[:eep] if $opts[:props].any? 'all'
49
+
50
+ # Iterate the SQLite3::ResultSet once
51
+ stmt.execute.each_with_index { |i, j| rows[j] = i }
52
+ stmt.close
53
+
54
+ # Close the database
55
+ db.close
56
+ $log.debug("Closed #{db_file}")
57
+
58
+ rescue SQLite3::BusyException => e
59
+ $log.error("Non-fatal error closing database #{db.filename}")
60
+
61
+ rescue SQLite3::Exception => e
62
+ abort("#{db_file}: #{e}")
63
+
64
+ end
65
+
66
+ return(rows)
67
+ end
68
+
32
69
  # @param obj [ICalPal] A +Store+ or +Calendar+
33
70
  def initialize(obj)
34
71
  obj['type'] = EventKit::EKSourceType.find_index { |i| i[:name] == 'Subscribed' } if obj['subcal_url']
@@ -43,6 +80,40 @@ module ICalPal
43
80
  @self = obj
44
81
  end
45
82
 
83
+ # Create a new CSV::Row with values from +self+. Newlines are
84
+ # replaced with '\n' to ensure each Row is a single line of text.
85
+ #
86
+ # @param headers [Array] Key names used as the header row in a CSV::Table
87
+ # @return [CSV::Row] The +Store+, +Calendar+, or +CalendarItem+ as a CSV::Row
88
+ def to_csv(headers)
89
+ values = []
90
+ headers.each { |h| values.push(@self[h].respond_to?(:gsub)? @self[h].gsub(/\n/, '\n') : @self[h]) }
91
+
92
+ CSV::Row::new(headers, values)
93
+ end
94
+
95
+ # @return [String] All fields in a simple XML format: <field>value</field>.
96
+ # Fields with empty values return <field/>.
97
+ def to_xml
98
+ 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
113
+
114
+ retval
115
+ end
116
+
46
117
  # Get the +n+'th +dow+ in month +m+
47
118
  #
48
119
  # @param n [Integer] Integer between -4 and +4
data/lib/options.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'optparse'
2
2
 
3
3
  require_relative 'defaults'
4
+ require_relative 'version'
4
5
 
5
6
  module ICalPal
6
7
  # Handle program options from all sources:
@@ -23,7 +24,7 @@ module ICalPal
23
24
  @op = OptionParser.new
24
25
  @op.summary_width = 23
25
26
  @op.banner += " [-c] COMMAND"
26
- @op.version = '1.2.0'
27
+ @op.version = ICalPal::VERSION
27
28
 
28
29
  @op.accept(ICalPal::RDT) { |s| ICalPal::RDT.conv(s) }
29
30
 
@@ -31,6 +32,7 @@ module ICalPal
31
32
  @op.on("\nCOMMAND must be one of the following:\n\n")
32
33
 
33
34
  @op.on("%s%s %sPrint events" % pad('events'))
35
+ @op.on("%s%s %sPrint tasks" % pad('tasks'))
34
36
  @op.on("%s%s %sPrint calendars" % pad('calendars'))
35
37
  @op.on("%s%s %sPrint accounts" % pad('accounts'))
36
38
 
@@ -38,16 +40,20 @@ module ICalPal
38
40
  @op.on("%s%s %sPrint events occurring today" % pad('eventsToday'))
39
41
  @op.on("%s%s %sPrint events occurring between today and NUM days into the future" % pad('eventsToday+NUM'))
40
42
  @op.on("%s%s %sPrint events occurring at present time" % pad('eventsNow'))
43
+ @op.on("%s%s %sPrint tasks with a due date" % pad('datedTasks'))
44
+ @op.on("%s%s %sPrint tasks with no due date" % pad('undatedTasks'))
41
45
 
42
46
  # global
43
47
  @op.separator("\nGlobal options:\n\n")
44
48
 
45
49
  @op.on('-c=COMMAND', '--cmd=COMMAND', COMMANDS, 'Command to run')
46
- @op.on('--db=DB', 'Use DB file instead of Calendar')
50
+ @op.on('--db=DB', "Use DB file instead of Calendar (default: #{$defaults[:common][:db]})",
51
+ 'For the tasks commands this should be a directory containing .sqlite files',
52
+ "(default: #{$defaults[:tasks][:db]})")
47
53
  @op.on('--cf=FILE', "Set config file path (default: #{$defaults[:common][:cf]})")
48
54
  @op.on('-o', '--output=FORMAT', OUTFORMATS,
49
55
  "Print as FORMAT (default: #{$defaults[:common][:output]})", "[#{OUTFORMATS.join(', ')}]")
50
-
56
+
51
57
  # include/exclude
52
58
  @op.separator("\nIncluding/excluding calendars:\n\n")
53
59
 
@@ -63,6 +69,10 @@ module ICalPal
63
69
  @op.on('--ic=CALENDARS', Array, 'List of calendars to include')
64
70
  @op.on('--ec=CALENDARS', Array, 'List of calendars to exclude')
65
71
 
72
+ @op.separator('')
73
+ @op.on('--il=LISTS', Array, 'List of reminder lists to include')
74
+ @op.on('--el=LISTS', Array, 'List of reminder lists to exclude')
75
+
66
76
  # dates
67
77
  @op.separator("\nChoosing dates:\n\n")
68
78
 
@@ -85,10 +95,11 @@ module ICalPal
85
95
  @op.on('--eep=PROPERTIES', Array, 'List of properties to exclude')
86
96
  @op.on('--aep=PROPERTIES', Array, 'List of properties to include in addition to the default list')
87
97
  @op.separator('')
88
- # @op.on('--itp=PROPERTIES', Array, 'List of task properties to include')
89
- # @op.on('--etp=PROPERTIES', Array, 'List of task properties to exclude')
90
- # @op.on('--atp=PROPERTIES', Array, 'List of task properties to include in addition to the default list')
91
- # @op.separator('')
98
+ @op.on('--itp=PROPERTIES', Array, 'List of task properties to include')
99
+ @op.on('--etp=PROPERTIES', Array, 'List of task properties to exclude')
100
+ @op.on('--atp=PROPERTIES', Array, 'List of task properties to include in addition to the default list',
101
+ 'Included for backwards compatability, these are aliases for --iep, --eep, and --aep')
102
+ @op.separator('')
92
103
 
93
104
  @op.on('--uid', 'Show event UIDs')
94
105
  @op.on('--eed', 'Exclude end datetimes')
@@ -114,13 +125,12 @@ module ICalPal
114
125
  @op.separator('')
115
126
  @op.on('--sc', 'Separate by calendar')
116
127
  @op.on('--sd', 'Separate by date')
117
- # @op.on('--sp', 'Separate by priority')
118
- # @op.on('--sta', 'Sort tasks by due date (ascending)')
119
- # @op.on('--std', 'Sort tasks by due date (descending)')
120
- # @op.separator('')
128
+ @op.on('--sp', 'Separate by priority')
121
129
  @op.on('--sep=PROPERTY', 'Separate by PROPERTY')
122
130
  @op.separator('')
123
131
  @op.on('--sort=PROPERTY', 'Sort by PROPERTY')
132
+ @op.on('--std', 'Sort tasks by due date (same as --sort=due_date)')
133
+ @op.on('--stda', 'Sort tasks by due date (ascending) (same as --sort=due_date -r)')
124
134
  @op.on('-r', '--reverse', 'Sort in reverse')
125
135
 
126
136
  @op.separator('')
@@ -134,6 +144,7 @@ module ICalPal
134
144
 
135
145
  @op.separator('')
136
146
  @op.on('-b', '--bullet=STRING', String, 'Use STRING for bullets')
147
+ @op.on('--ab=STRING', String, 'Use STRING for alert bullets')
137
148
  @op.on('--nb', 'Do not use bullets')
138
149
  @op.on('--nnr=SEPARATOR', String, 'Set replacement for newlines within notes')
139
150
 
@@ -202,10 +213,21 @@ module ICalPal
202
213
  .merge(env)
203
214
  .merge(cli)
204
215
 
216
+ # datedTasks and undatedTasks
217
+ opts[:cmd] = "tasks" if opts[:cmd] == "datedTasks"
218
+ opts[:cmd] = "tasks" if opts[:cmd] == "undatedTasks"
219
+
205
220
  # All kids love log!
206
221
  $log.level = opts[:debug]
207
222
 
223
+ # For posterity
224
+ opts[:ruby] = RUBY_VERSION
225
+ opts[:version] = @op.version
226
+
208
227
  # From the Department of Redundancy Department
228
+ opts[:iep] += opts[:itp] if opts[:itp]
229
+ opts[:eep] += opts[:etp] if opts[:etp]
230
+ opts[:aep] += opts[:atp] if opts[:atp]
209
231
  opts[:props] = (opts[:iep] + opts[:aep] - opts[:eep]).uniq
210
232
 
211
233
  # From, to, days
@@ -217,6 +239,10 @@ module ICalPal
217
239
  opts[:from] = $now if opts[:n]
218
240
  end
219
241
 
242
+ # Sorting
243
+ opts[:sort] = 'due_date' if opts[:std] or opts[:stda]
244
+ opts[:reverse] = true if opts[:std]
245
+
220
246
  # Colors
221
247
  opts[:palette] = 8 if opts[:f]
222
248
  opts[:palette] = 24 if opts[:color]
@@ -225,7 +251,7 @@ module ICalPal
225
251
  unless opts[:sep]
226
252
  opts[:sep] = 'calendar' if opts[:sc]
227
253
  opts[:sep] = 'sday' if opts[:sd]
228
- opts[:sep] = 'priority' if opts[:sp]
254
+ opts[:sep] = 'long_priority' if opts[:sp]
229
255
  end
230
256
  opts[:nc] = true if opts[:sc]
231
257
 
@@ -234,17 +260,6 @@ module ICalPal
234
260
  raise(OptionParser::InvalidOption, 'Start date must be before end date') if opts[:from] && opts[:from] > opts[:to]
235
261
  raise(OptionParser::MissingArgument, 'No properties to display') if opts[:props].empty?
236
262
 
237
- # Open the database here so we can catch errors and print the help message
238
- $log.debug("Opening database: #{opts[:db]}")
239
- $db = SQLite3::Database.new(opts[:db], { readonly: true, results_as_hash: true })
240
- $db.prepare('SELECT 1 FROM Calendar LIMIT 1').close
241
-
242
- rescue SQLite3::SQLException => e
243
- @op.abort("#{opts[:db]} is not a Calendar database")
244
-
245
- rescue SQLite3::Exception => e
246
- @op.abort("#{opts[:db]}: #{e}")
247
-
248
263
  rescue StandardError => e
249
264
  @op.abort("#{e}\n\n#{@op.help}\n#{e}")
250
265
  end
@@ -253,10 +268,10 @@ module ICalPal
253
268
  end
254
269
 
255
270
  # Commands that can be run
256
- COMMANDS = %w{events eventsToday eventsNow calendars accounts stores}
271
+ COMMANDS = %w{events eventsToday eventsNow tasks datedTasks undatedTasks calendars accounts stores}
257
272
 
258
273
  # Supported output formats
259
- OUTFORMATS = %w{ansi csv default hash html json md rdoc toc yaml remind}
274
+ OUTFORMATS = %w{ansi csv default hash html json md rdoc remind toc xml yaml}
260
275
 
261
276
  private
262
277
 
data/lib/rdt.rb CHANGED
@@ -52,5 +52,12 @@ module ICalPal
52
52
  def to_i
53
53
  to_time.to_i
54
54
  end
55
+
56
+ # @see ICalPal::RDT.to_s
57
+ #
58
+ # @return [Boolean]
59
+ def ==(obj)
60
+ self.to_s == obj.to_s
61
+ end
55
62
  end
56
63
  end
data/lib/reminder.rb ADDED
@@ -0,0 +1,110 @@
1
+ require 'open3'
2
+ require 'nokogiri-plist'
3
+
4
+ module ICalPal
5
+ # Class representing items from the <tt>Reminders</tt> database
6
+ class Reminder
7
+ include ICalPal
8
+
9
+ def [](k)
10
+ case k
11
+ when 'notes' then # Skip empty notes
12
+ @self['notes'].length > 0? @self['notes'] : nil
13
+
14
+ when 'priority' then # Integer -> String
15
+ EventKit::EKReminderProperty[@self['priority']] if @self['priority'] > 0
16
+
17
+ when 'sdate' then # For sorting
18
+ @self['title']
19
+
20
+ else @self[k]
21
+ end
22
+ end
23
+
24
+ def initialize(obj)
25
+ @self = {}
26
+ obj.keys.each { |k| @self[k] = obj[k] }
27
+
28
+ # Priority
29
+ @self['prio'] = 0 if @self['priority'] == 1 # high
30
+ @self['prio'] = 1 if @self['priority'] == 5 # medium
31
+ @self['prio'] = 2 if @self['priority'] == 9 # low
32
+ @self['prio'] = 3 if @self['priority'] == 0 # none
33
+
34
+ @self['long_priority'] = LONG_PRIORITY[@self['prio']]
35
+
36
+ # For sorting
37
+ @self['sdate'] = (@self['title'])? @self['title'] : ""
38
+
39
+ # Due date
40
+ @self['due'] = RDT.new(*Time.at(@self['due_date'] + ITIME).to_a.reverse[4..]) if @self['due_date']
41
+ @self['due_date'] = 0 unless @self['due_date']
42
+
43
+ # Notes
44
+ @self['notes'] = "" unless @self['notes']
45
+
46
+ # Color
47
+ @self['color'] = nil unless $opts[:palette]
48
+
49
+ if @self['color'] then
50
+ # Run command
51
+ stdin, stdout, stderr, e = Open3.popen3(PL_CONVERT)
52
+
53
+ # Send color bplist
54
+ stdin.write(@self['color'])
55
+ stdin.close
56
+
57
+ # Read output
58
+ plist = Nokogiri::PList(stdout.read)['$objects']
59
+
60
+ @self['color'] = plist[3]
61
+ @self['symbolic_color_name'] = (plist[2] == 'custom')? plist[4] : plist[2]
62
+ else
63
+ @self['color'] = DEFAULT_COLOR
64
+ @self['symbolic_color_name'] = DEFAULT_SYMBOLIC_COLOR
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ DEFAULT_COLOR = '#1BADF8'
71
+ DEFAULT_SYMBOLIC_COLOR = 'blue'
72
+
73
+ LONG_PRIORITY = [
74
+ "High priority",
75
+ "Medium priority",
76
+ "Low priority",
77
+ "No priority",
78
+ ]
79
+
80
+ PL_CONVERT = '/usr/bin/plutil -convert xml1 -o - -'
81
+
82
+ DB_PATH = "#{Dir::home}/Library/Group Containers/group.com.apple.reminders/Container_v1/Stores"
83
+
84
+ QUERY = <<~SQL
85
+ SELECT DISTINCT
86
+
87
+ zremcdReminder.zAllday as all_day,
88
+ zremcdReminder.zDuedate as due_date,
89
+ zremcdReminder.zFlagged as flagged,
90
+ zremcdReminder.zNotes as notes,
91
+ zremcdReminder.zPriority as priority,
92
+ zremcdReminder.zTitle as title,
93
+
94
+ zremcdBaseList.zBadgeEmblem as badge,
95
+ zremcdBaseList.zColor as color,
96
+ zremcdBaseList.zName as list_name,
97
+ zremcdBaseList.zParentList as parent,
98
+ zremcdBaseList.zSharingStatus as shared
99
+
100
+ FROM zremcdReminder
101
+
102
+ JOIN zremcdBaseList ON zremcdReminder.zList = zremcdBaseList.z_pk
103
+
104
+ WHERE zremcdReminder.zCompleted = 0
105
+ AND zremcdReminder.zMarkedForDeletion = 0
106
+
107
+ SQL
108
+
109
+ end
110
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module ICalPal
2
+ VERSION = '2.0.0'
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: 1.2.0
4
+ version: 2.0.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-05 00:00:00.000000000 Z
11
+ date: 2024-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sqlite3
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri-plist
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.0
27
41
  description: |
28
42
  Inspired by icalBuddy and maintains close compatability. Includes
29
43
  many additional features for querying, filtering, and formatting.
@@ -45,7 +59,9 @@ files:
45
59
  - lib/icalPal.rb
46
60
  - lib/options.rb
47
61
  - lib/rdt.rb
62
+ - lib/reminder.rb
48
63
  - lib/store.rb
64
+ - lib/version.rb
49
65
  homepage: https://github.com/ajrosen/icalPal
50
66
  licenses:
51
67
  - GPL-3.0-or-later