icalPal 1.2.1 → 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: 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