icalPal 1.2.1 → 2.0.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: c08ab4aaf0558f9648fbefbb9eaea1aac9c50de2770e1897699b619ba92ef6b6
4
- data.tar.gz: '087d14b6e855bdbf2681e526941baec600fed607a6e6760b09f4361bb33735d8'
3
+ metadata.gz: 7f91434d02dd357bcd02342b2d79418b91e23ec789a6a069a01f2f508685d8ca
4
+ data.tar.gz: 51cfa58c767650d0c7e62213ad4a58dbe9a2d5fc42f17d5ce18c8b65e6a178be
5
5
  SHA512:
6
- metadata.gz: 70934664b432d1b99e0f200bd1e78a2b142f9b1b9b405c8763913aa9d414a83171f4876e0145ff741854abd1b643fdb4a72031b564af14a2a4f1764cc494a13f
7
- data.tar.gz: '0580e0d9556919516b1c2d4a4ea11306a2a8786ea80ee5ccc5821c97f899d7bc95c3499c7f5d9dc05ac091dba53867b01a7b676e0bd20881563d7529e16a1a31'
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,32 +54,17 @@ 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
- begin
63
- stmt = $db.prepare(q)
64
- abort(stmt.columns.sort.join(' ')) if $opts[:props].any? 'list'
65
- $opts[:props] = stmt.columns - $opts[:eep] if $opts[:props].any? 'all'
66
-
67
- # Iterate the SQLite3::ResultSet once
68
- stmt.execute.each_with_index { |i, j| $rows[j] = i }
69
- stmt.close
70
-
71
- # Close the database
72
- $db.close
73
- $log.debug("Closed #{$opts[:db]}")
74
-
75
- rescue SQLite3::BusyException => e
76
- $log.error("Non-fatal error closing database #{$db.filename}")
77
-
78
- rescue SQLite3::SQLException => e
79
- abort(e.message)
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)
80
65
  end
81
66
 
82
- $log.info("Loaded #{$rows.count} rows from #{$opts[:db]}")
67
+ $log.info("Loaded #{$rows.count} #{klass} rows")
83
68
 
84
69
 
85
70
  ##################################################
@@ -111,6 +96,7 @@ $rows.each_with_index do |row, i|
111
96
  next
112
97
  end
113
98
 
99
+ # Instantiate an item
114
100
  item = klass.new(row)
115
101
 
116
102
  # --et/--it
@@ -124,11 +110,18 @@ $rows.each_with_index do |row, i|
124
110
  next
125
111
  end
126
112
 
127
- unless ICalPal::Event === item
128
- # Always add non-event items
129
- $log.debug("Adding non-event #{item}")
130
- add(item)
131
- else
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
132
125
  # Check for all-day and cancelled events
133
126
  if $opts[:ea] && item['all_day'].positive? then
134
127
  $log.debug(":ea")
@@ -148,6 +141,21 @@ $rows.each_with_index do |row, i|
148
141
  (item['has_recurrences'].positive?)?
149
142
  item.recurring.each { |i| add(i) } :
150
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)
151
159
  end
152
160
  end
153
161
 
@@ -163,13 +171,13 @@ end
163
171
 
164
172
  # Sort the rows
165
173
  begin
166
- $log.debug("Sorting/uniqing #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
174
+ $log.info("Sorting/uniqing #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
167
175
 
168
176
  $items.sort_by! { |i| [ i[$opts[:sep]], i[$opts[:sort]], i['sdate'] ] }
169
177
  $items.reverse! if $opts[:reverse]
170
178
  $items.uniq!
171
- rescue ArgumentError => e
172
- $log.warn("Sorting failed, results may be unexpected\n")
179
+ rescue Exception => e
180
+ $log.info("Sorting failed: #{e}\n")
173
181
  end
174
182
 
175
183
  $log.debug("#{$items.count} items remain")
@@ -211,6 +219,9 @@ unless mu
211
219
  table
212
220
  when 'hash' then items.map { |i| i.self }
213
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]}>"
214
225
  when 'yaml' then items.map { |i| i.self }.to_yaml
215
226
  when 'remind' then items.map { |i|
216
227
  "REM #{i['sdate'].strftime('%F AT %R')} " +
@@ -225,7 +236,8 @@ end
225
236
 
226
237
  $log.debug("Formatting with #{mu.inspect}")
227
238
 
228
- section = nil if $opts[:sep]
239
+ doc = RDoc::Markup::Document.new
240
+ section = nil
229
241
 
230
242
  items.each_with_index do |i, j|
231
243
  $log.debug("Print #{j}: #{i.inspect}")
@@ -233,15 +245,16 @@ items.each_with_index do |i, j|
233
245
  # --li
234
246
  break if $opts[:li].positive? && j >= $opts[:li]
235
247
 
236
- 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
237
252
 
238
253
  # Sections
239
254
  if $opts[:sep] && section != i[$opts[:sep]]
240
255
  $log.debug("New section '#{$opts[:sep]}': #{i[$opts[:sep]]}")
241
256
 
242
- v = RDoc::Markup::Verbatim.new
243
- v.format = { item: i, prop: $opts[:sep] }
244
- doc << v
257
+ doc << RDoc::Markup::Raw.new($opts[:sep])
245
258
 
246
259
  doc << RDoc::Markup::BlankLine.new if j.positive?
247
260
  doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
@@ -255,27 +268,30 @@ items.each_with_index do |i, j|
255
268
 
256
269
  # Properties
257
270
  $opts[:props].each_with_index do |prop, k|
258
- next unless i[prop]
259
- next if Array === i[prop] && !i[prop][0]
271
+ value = i[prop]
272
+
273
+ next unless value
274
+ next if Array === value && !value[0]
275
+ next if String === value && value.length == 0
260
276
 
261
- $log.debug("#{prop}: #{i[prop]}")
277
+ $log.debug("#{prop}: #{value}")
262
278
 
263
- v = RDoc::Markup::Verbatim.new
264
- v.format = { item: i, prop: prop }
265
- props << v
279
+ # Use Raw to save the property
280
+ props << RDoc::Markup::Raw.new(prop)
266
281
 
267
282
  unless k.positive?
268
283
  # First property, value only
269
- props << RDoc::Markup::Heading.new(2, i[prop].to_s)
284
+ props << RDoc::Markup::Heading.new(2, value.to_s)
270
285
  else
271
286
  props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
272
- props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(i[prop])) unless(i['placeholder'])
287
+ props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless(i['placeholder'])
273
288
  end
274
289
  end
275
290
 
276
291
  # Print it
277
- unless props.empty?
278
- doc << props
279
- puts doc.accept(mu)
280
- end
292
+ props << RDoc::Markup::BlankLine.new unless props.empty?
293
+
294
+ doc << props
281
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.1"
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/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']
@@ -55,6 +92,28 @@ module ICalPal
55
92
  CSV::Row::new(headers, values)
56
93
  end
57
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
+
58
117
  # Get the +n+'th +dow+ in month +m+
59
118
  #
60
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.1'
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/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.1
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-06 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