icalPal 1.2.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +97 -46
- data/bin/icalPal +82 -57
- data/icalPal.gemspec +4 -1
- data/lib/EventKit.rb +7 -0
- data/lib/ToICalPal.rb +43 -17
- data/lib/defaults.rb +21 -5
- data/lib/event.rb +2 -0
- data/lib/icalPal.rb +86 -3
- data/lib/options.rb +40 -25
- data/lib/reminder.rb +110 -0
- data/lib/version.rb +3 -0
- metadata +19 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4dab143a05d8a63ae36b721ca6ba53180c75d8b4dd0f0cebdc84fb533968c248
|
|
4
|
+
data.tar.gz: 8d243a120762c90059e4206a2deb5e79b821e558cf9ad9a437ad84fbfbb6a21b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24f4b88c592e59f2ffbc116761d6da41b60a7e5d4554feea9bdb023450967eaa7921f5c4708a66bf76554926ab4e0ca52965470f7576cef1cce5aec9a6112cbb
|
|
7
|
+
data.tar.gz: d69cc1d2cd63072020a4059a352ccdb2262a189e10c3c4e1891bd1c953ea2cb60a577fc83bd2ab55b561d0a752b0fb4198495bc2a5f57909d4216101cc7ea012
|
data/README.md
CHANGED
|
@@ -4,16 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
## Description
|
|
6
6
|
|
|
7
|
-
icalPal is a command-line tool to query a macOS Calendar
|
|
8
|
-
accounts, calendars, and
|
|
9
|
-
[Ruby](https://www.ruby-lang.org/) and access to a
|
|
10
|
-
|
|
7
|
+
icalPal is a command-line tool to query a macOS Calendar and Reminders
|
|
8
|
+
databases for accounts, calendars, events, and tasks. It can be run
|
|
9
|
+
on any system with [Ruby](https://www.ruby-lang.org/) and access to a
|
|
10
|
+
Calendar or Reminders database.
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
|
+
As a system-wide Ruby gem:
|
|
15
|
+
|
|
14
16
|
```
|
|
15
17
|
gem install icalPal
|
|
16
|
-
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
or in your home diretory:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
gem install --user-install icalPal
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
As a Homebrew formula:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
brew tap ajrosen/icalPal
|
|
30
|
+
brew install icalPal
|
|
17
31
|
```
|
|
18
32
|
|
|
19
33
|
## Features
|
|
@@ -21,11 +35,14 @@ icalPal events
|
|
|
21
35
|
### Compatability with [icalBuddy](https://github.com/ali-rantakari/icalBuddy)
|
|
22
36
|
|
|
23
37
|
icalPal tries to be compatible with icalBuddy for command-line options
|
|
24
|
-
and for output. There are a
|
|
38
|
+
and for output. There are a some important differences to be aware
|
|
39
|
+
of.
|
|
25
40
|
|
|
26
41
|
* Options require two hyphens, except for single-letter options that require one hyphen
|
|
27
42
|
* *eventsFrom* is not supported. Instead there is *--from*, *--to*, and *--days*
|
|
28
|
-
*
|
|
43
|
+
* *uncompletedTasks* is simply *tasks*
|
|
44
|
+
* *undatedUncompletedTasks* is simply *undatedTasks*
|
|
45
|
+
* *tasksDueBefore:DATE* is not yet supported
|
|
29
46
|
* The command can go anywhere; it doesn't have to be the last argument
|
|
30
47
|
* Property separators are comma-delimited
|
|
31
48
|
|
|
@@ -35,6 +52,10 @@ and for output. There are a few differences to be aware of.
|
|
|
35
52
|
|
|
36
53
|
Shows a list of enabled Calendar accounts. Internally they are known as *Stores*; you can run ```icalPal stores``` instead.
|
|
37
54
|
|
|
55
|
+
```icalPal datedTasks```
|
|
56
|
+
|
|
57
|
+
Shows only reminders that have a due date.
|
|
58
|
+
|
|
38
59
|
### Additional options
|
|
39
60
|
|
|
40
61
|
* 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 +63,45 @@ Shows a list of enabled Calendar accounts. Internally they are known as *Stores
|
|
|
42
63
|
* Use ```-o``` to print the output in different formats. CSV or JSON are intertesting choices.
|
|
43
64
|
* Copy your Calendar database file and use ```--db``` on it.
|
|
44
65
|
* ```--it``` and ```--et``` will filter by Calendar *type*. Types are **Local**, **Exchange**, **CalDAV**, **MobileMe**, **Subscribed**, and **Birthdays**
|
|
66
|
+
* ```--il``` and ```-el``` will filter by Reminder list
|
|
45
67
|
* ```--ia``` includes *only* all-day events (opposite of ```--ea```)
|
|
46
68
|
* ```--aep``` is like ```--iep```, but *adds* to the default property list instead of replacing it.
|
|
47
69
|
* ```--sep``` to separate by any property, not just calendar (```--sc```) or date (```--sd```)
|
|
48
70
|
* ```--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
71
|
|
|
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
|
|
72
|
+
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
73
|
|
|
52
74
|
## Usage
|
|
53
75
|
|
|
54
76
|
icalPal: Usage: icalPal [options] [-c] COMMAND
|
|
55
77
|
|
|
56
78
|
COMMAND must be one of the following:
|
|
57
|
-
|
|
79
|
+
```
|
|
58
80
|
events Print events
|
|
81
|
+
tasks Print tasks
|
|
59
82
|
calendars Print calendars
|
|
60
83
|
accounts Print accounts
|
|
61
84
|
|
|
62
85
|
eventsToday Print events occurring today
|
|
63
86
|
eventsToday+NUM Print events occurring between today and NUM days into the future
|
|
64
87
|
eventsNow Print events occurring at present time
|
|
88
|
+
datedTasks Print tasks with a due date
|
|
89
|
+
undatedTasks Print tasks with no due date
|
|
90
|
+
```
|
|
65
91
|
|
|
66
92
|
Global options:
|
|
67
|
-
|
|
93
|
+
```
|
|
68
94
|
-c, --cmd=COMMAND Command to run
|
|
69
|
-
--db=DB Use DB file instead of Calendar
|
|
70
|
-
|
|
95
|
+
--db=DB Use DB file instead of Calendar (default: /Users/ajr/Library/Calendars/Calendar.sqlitedb)
|
|
96
|
+
For the tasks commands this should be a directory containing .sqlite files
|
|
97
|
+
(default: /Users/ajr/Library/Group Containers/group.com.apple.reminders/Container_v1/Stores)
|
|
98
|
+
--cf=FILE Set config file path (default: /Users/ajr/.icalPal)
|
|
71
99
|
-o, --output=FORMAT Print as FORMAT (default: default)
|
|
72
|
-
[ansi, csv, default, hash, html, json, md, rdoc, toc,
|
|
73
|
-
|
|
74
|
-
Including/excluding calendars:
|
|
100
|
+
[ansi, csv, default, hash, html, json, md, rdoc, remind, toc, xml, yaml]
|
|
101
|
+
```
|
|
75
102
|
|
|
103
|
+
Including/excluding calendars and reminders:
|
|
104
|
+
```
|
|
76
105
|
--is=ACCOUNTS List of accounts to include
|
|
77
106
|
--es=ACCOUNTS List of accounts to exclude
|
|
78
107
|
|
|
@@ -83,8 +112,12 @@ Including/excluding calendars:
|
|
|
83
112
|
--ic=CALENDARS List of calendars to include
|
|
84
113
|
--ec=CALENDARS List of calendars to exclude
|
|
85
114
|
|
|
86
|
-
|
|
115
|
+
--il=LISTS List of reminder lists to include
|
|
116
|
+
--el=LISTS List of reminder lists to exclude
|
|
117
|
+
```
|
|
87
118
|
|
|
119
|
+
Choosing dates:
|
|
120
|
+
```
|
|
88
121
|
--from=DATE List events starting on or after DATE
|
|
89
122
|
--to=DATE List events starting on or before DATE
|
|
90
123
|
DATE can be yesterday, today, tomorrow, +N, -N, or anything accepted by DateTime.parse()
|
|
@@ -95,13 +128,19 @@ Choosing dates:
|
|
|
95
128
|
--sed Show empty dates with --sd
|
|
96
129
|
--ia Include only all-day events
|
|
97
130
|
--ea Exclude all-day events
|
|
131
|
+
```
|
|
98
132
|
|
|
99
133
|
Choose properties to include in the output:
|
|
100
|
-
|
|
134
|
+
```
|
|
101
135
|
--iep=PROPERTIES List of properties to include
|
|
102
136
|
--eep=PROPERTIES List of properties to exclude
|
|
103
137
|
--aep=PROPERTIES List of properties to include in addition to the default list
|
|
104
138
|
|
|
139
|
+
--itp=PROPERTIES List of task properties to include
|
|
140
|
+
--etp=PROPERTIES List of task properties to exclude
|
|
141
|
+
--atp=PROPERTIES List of task properties to include in addition to the default list
|
|
142
|
+
Included for backwards compatability, these are aliases for --iep, --eep, and --aep
|
|
143
|
+
|
|
105
144
|
--uid Show event UIDs
|
|
106
145
|
--eed Exclude end datetimes
|
|
107
146
|
|
|
@@ -113,16 +152,20 @@ Choose properties to include in the output:
|
|
|
113
152
|
|
|
114
153
|
Use 'all' for PROPERTIES to include all available properties (except any listed in --eep)
|
|
115
154
|
Use 'list' for PROPERTIES to list all available properties and exit
|
|
155
|
+
```
|
|
116
156
|
|
|
117
157
|
Formatting the output:
|
|
118
|
-
|
|
158
|
+
```
|
|
119
159
|
--li=N Show at most N items (default: 0 for no limit)
|
|
120
160
|
|
|
121
161
|
--sc Separate by calendar
|
|
122
162
|
--sd Separate by date
|
|
163
|
+
--sp Separate by priority
|
|
123
164
|
--sep=PROPERTY Separate by PROPERTY
|
|
124
165
|
|
|
125
166
|
--sort=PROPERTY Sort by PROPERTY
|
|
167
|
+
--std Sort tasks by due date (same as --sort=due_date)
|
|
168
|
+
--stda Sort tasks by due date (ascending) (same as --sort=due_date -r)
|
|
126
169
|
-r, --reverse Sort in reverse
|
|
127
170
|
|
|
128
171
|
--ps=SEPARATORS List of property separators
|
|
@@ -133,53 +176,37 @@ Formatting the output:
|
|
|
133
176
|
See https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/DateTime.html#method-i-strftime for details
|
|
134
177
|
|
|
135
178
|
-b, --bullet=STRING Use STRING for bullets
|
|
179
|
+
--ab=STRING Use STRING for alert bullets
|
|
180
|
+
--nb Do not use bullets
|
|
136
181
|
--nnr=SEPARATOR Set replacement for newlines within notes
|
|
137
182
|
|
|
138
183
|
-f Format output using standard ANSI colors
|
|
139
184
|
--color Format output using a larger color palette
|
|
185
|
+
```
|
|
140
186
|
|
|
141
187
|
Help:
|
|
142
|
-
|
|
188
|
+
```
|
|
143
189
|
-h, --help Show this message
|
|
144
|
-
-V, -v, --version Show version and exit (
|
|
190
|
+
-V, -v, --version Show version and exit (2.0.0)
|
|
145
191
|
-d, --debug=LEVEL Set the logging level (default: warn)
|
|
146
192
|
[debug, info, warn, error, fatal]
|
|
193
|
+
```
|
|
147
194
|
|
|
148
195
|
Environment variables:
|
|
149
|
-
|
|
196
|
+
```
|
|
150
197
|
ICALPAL Additional arguments
|
|
151
198
|
ICALPAL_CONFIG Additional arguments from a file
|
|
152
|
-
(default:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
## History
|
|
156
|
-
|
|
157
|
-
I have used icalBuddy for many years. It's great for scripting,
|
|
158
|
-
automation, and as a desktop widget for apps like
|
|
159
|
-
[GeekTool](https://www.tynsoe.org/geektool/) and
|
|
160
|
-
[Übersicht](https://tracesof.net/uebersicht/).
|
|
161
|
-
|
|
162
|
-
As with many applications, I started to run into some limitations in
|
|
163
|
-
icalBuddy. The biggest being that active development ended in 2014.
|
|
164
|
-
It's only thanks to the efforts of [Jim
|
|
165
|
-
Lawton](https://github.com/jimlawton) that it even compiles anymore.
|
|
166
|
-
|
|
167
|
-
Instead of trying to understand and extend the existing code, I chose
|
|
168
|
-
to start anew using my language of choice. Using Ruby means icalPal
|
|
169
|
-
is multi-platform. It also meant *much* less code; about 1,200 lines
|
|
170
|
-
vs. 7,000.
|
|
171
|
-
|
|
172
|
-
I won't pretend to understand **why** you would want this on Linux or
|
|
173
|
-
Windows. But since icalPal is written in Ruby and gets its data
|
|
174
|
-
directly from the Calendar database file instead of an API, you *can*.
|
|
199
|
+
(default: /Users/ajr/.icalPal)
|
|
200
|
+
```
|
|
175
201
|
|
|
176
202
|
## Output formats
|
|
177
203
|
|
|
178
204
|
icalPal supports several output formats. The **default** format tries
|
|
179
205
|
to mimic icalBuddy as much as possible.
|
|
180
206
|
|
|
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
|
|
207
|
+
CSV, Hash, JSON, XML, and YAML print all fields for all items in their
|
|
208
|
+
respective formats. From that you can analyze the results any way you
|
|
209
|
+
like.
|
|
183
210
|
|
|
184
211
|
[Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built in icalPal.
|
|
185
212
|
|
|
@@ -209,5 +236,29 @@ objects, one for each of the item's properties:
|
|
|
209
236
|
|
|
210
237
|
The document will also include a number of
|
|
211
238
|
[RDoc::Markup::Verbatim](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Verbatim.html)
|
|
239
|
+
and
|
|
240
|
+
[RDoc::Markup::Raw](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup/Raw.html)
|
|
212
241
|
items. They are not included in the output, but are used to pass
|
|
213
242
|
information about the item and property to the default formatter.
|
|
243
|
+
|
|
244
|
+
## History
|
|
245
|
+
|
|
246
|
+
I used icalBuddy for many years. It's great for scripting,
|
|
247
|
+
automation, and as a desktop widget for apps like
|
|
248
|
+
[GeekTool](https://www.tynsoe.org/geektool/) and
|
|
249
|
+
[Übersicht](https://tracesof.net/uebersicht/).
|
|
250
|
+
|
|
251
|
+
As with many applications, I started to run into some limitations in
|
|
252
|
+
icalBuddy. The biggest being that active development ended in 2014.
|
|
253
|
+
It's only thanks to the efforts of [Jim
|
|
254
|
+
Lawton](https://github.com/jimlawton) that it even compiles anymore.
|
|
255
|
+
|
|
256
|
+
Instead of trying to understand and extend the existing code, I chose
|
|
257
|
+
to start anew using my language of choice: Ruby. Using Ruby meant
|
|
258
|
+
there is *much* less code; about 1,600 lines vs. 7,000. It also means
|
|
259
|
+
icalPal is multi-platform.
|
|
260
|
+
|
|
261
|
+
I won't pretend to understand **why** you would want to run this on
|
|
262
|
+
Linux or Windows. But since icalPal is written in Ruby and gets its
|
|
263
|
+
data directly from the Calendar and Reminders database files instead
|
|
264
|
+
of an API, you *can*.
|
data/bin/icalPal
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
# *-*- mode: enh-ruby -*-*
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
begin
|
|
4
|
+
require 'logger'
|
|
5
|
+
|
|
6
|
+
require 'csv'
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'rdoc'
|
|
9
|
+
require 'sqlite3'
|
|
10
|
+
require 'yaml'
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
require 'yaml'
|
|
12
|
+
require_relative '../lib/icalPal'
|
|
13
|
+
require_relative '../lib/options'
|
|
14
|
+
rescue LoadError => e
|
|
15
|
+
dep = e.message[/-- (.*)/, 1]
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
$stderr.puts "FATAL: icalPal is missing a dependency: #{dep}"
|
|
18
|
+
$stderr.puts
|
|
19
|
+
$stderr.puts "Install with 'gem install --user-install #{dep}'"
|
|
20
|
+
|
|
21
|
+
exit
|
|
22
|
+
end
|
|
14
23
|
|
|
15
24
|
|
|
16
25
|
##################################################
|
|
@@ -44,7 +53,7 @@ $log.info("Options: #{$opts}")
|
|
|
44
53
|
def add(item)
|
|
45
54
|
$log.info("Adding #{item.inspect} #{item['UUID']} (#{item['title']})") if item['UUID']
|
|
46
55
|
|
|
47
|
-
item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if item['sdate']
|
|
56
|
+
item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if item === ICalPal::Event && item['sdate']
|
|
48
57
|
$items.push(item)
|
|
49
58
|
end
|
|
50
59
|
|
|
@@ -54,32 +63,17 @@ end
|
|
|
54
63
|
|
|
55
64
|
# What are we getting?
|
|
56
65
|
klass = ICalPal::($opts[:cmd])
|
|
57
|
-
q = klass::QUERY
|
|
58
|
-
|
|
59
|
-
$log.debug(q.gsub(/\n/, ' '))
|
|
60
66
|
|
|
61
67
|
# Get it
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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)
|
|
68
|
+
if klass == ICalPal::Reminder then
|
|
69
|
+
# Load all .sqlite files
|
|
70
|
+
Dir.glob("#{$opts[:db]}/*.sqlite").each { |db| $rows += ICalPal.load_data(db, klass::QUERY) }
|
|
71
|
+
else
|
|
72
|
+
# Load database
|
|
73
|
+
$rows += ICalPal.load_data($opts[:db], klass::QUERY)
|
|
80
74
|
end
|
|
81
75
|
|
|
82
|
-
$log.info("Loaded #{$rows.count}
|
|
76
|
+
$log.info("Loaded #{$rows.count} #{klass} rows")
|
|
83
77
|
|
|
84
78
|
|
|
85
79
|
##################################################
|
|
@@ -111,6 +105,7 @@ $rows.each_with_index do |row, i|
|
|
|
111
105
|
next
|
|
112
106
|
end
|
|
113
107
|
|
|
108
|
+
# Instantiate an item
|
|
114
109
|
item = klass.new(row)
|
|
115
110
|
|
|
116
111
|
# --et/--it
|
|
@@ -124,11 +119,18 @@ $rows.each_with_index do |row, i|
|
|
|
124
119
|
next
|
|
125
120
|
end
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
$log.debug("
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
# --el/--il
|
|
123
|
+
if $opts[:el].any? item['list_name'] then
|
|
124
|
+
$log.debug(":el")
|
|
125
|
+
next
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
unless $opts[:il].empty? or $opts[:il].any? item['list_name']
|
|
129
|
+
$log.debug(":il")
|
|
130
|
+
next
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if ICalPal::Event === item
|
|
132
134
|
# Check for all-day and cancelled events
|
|
133
135
|
if $opts[:ea] && item['all_day'].positive? then
|
|
134
136
|
$log.debug(":ea")
|
|
@@ -148,6 +150,21 @@ $rows.each_with_index do |row, i|
|
|
|
148
150
|
(item['has_recurrences'].positive?)?
|
|
149
151
|
item.recurring.each { |i| add(i) } :
|
|
150
152
|
item.non_recurring.each { |i| add(i) }
|
|
153
|
+
else
|
|
154
|
+
# Check for dated reminders
|
|
155
|
+
if ICalPal::Reminder === item then
|
|
156
|
+
if $opts[:dated] == 1 and item['due_date'] > 0 then
|
|
157
|
+
$log.debug(":undated")
|
|
158
|
+
next
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if $opts[:dated] == 2 and item['due_date'] == 0 then
|
|
162
|
+
$log.debug(":dated")
|
|
163
|
+
next
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
add(item)
|
|
151
168
|
end
|
|
152
169
|
end
|
|
153
170
|
|
|
@@ -163,13 +180,13 @@ end
|
|
|
163
180
|
|
|
164
181
|
# Sort the rows
|
|
165
182
|
begin
|
|
166
|
-
$log.
|
|
183
|
+
$log.info("Sorting/uniqing #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
|
|
167
184
|
|
|
168
185
|
$items.sort_by! { |i| [ i[$opts[:sep]], i[$opts[:sort]], i['sdate'] ] }
|
|
169
186
|
$items.reverse! if $opts[:reverse]
|
|
170
187
|
$items.uniq!
|
|
171
|
-
rescue
|
|
172
|
-
$log.
|
|
188
|
+
rescue Exception => e
|
|
189
|
+
$log.info("Sorting failed: #{e}\n")
|
|
173
190
|
end
|
|
174
191
|
|
|
175
192
|
$log.debug("#{$items.count} items remain")
|
|
@@ -211,6 +228,9 @@ unless mu
|
|
|
211
228
|
table
|
|
212
229
|
when 'hash' then items.map { |i| i.self }
|
|
213
230
|
when 'json' then items.map { |i| i.self }.to_json
|
|
231
|
+
when 'xml' then
|
|
232
|
+
xml = items.map { |i| "<#{$opts[:cmd].chomp("s")}>#{i.to_xml}</#{$opts[:cmd].chomp("s")}>" }
|
|
233
|
+
"<#{$opts[:cmd]}>\n#{xml.join("")}</#{$opts[:cmd]}>"
|
|
214
234
|
when 'yaml' then items.map { |i| i.self }.to_yaml
|
|
215
235
|
when 'remind' then items.map { |i|
|
|
216
236
|
"REM #{i['sdate'].strftime('%F AT %R')} " +
|
|
@@ -225,7 +245,8 @@ end
|
|
|
225
245
|
|
|
226
246
|
$log.debug("Formatting with #{mu.inspect}")
|
|
227
247
|
|
|
228
|
-
|
|
248
|
+
doc = RDoc::Markup::Document.new
|
|
249
|
+
section = nil
|
|
229
250
|
|
|
230
251
|
items.each_with_index do |i, j|
|
|
231
252
|
$log.debug("Print #{j}: #{i.inspect}")
|
|
@@ -233,15 +254,16 @@ items.each_with_index do |i, j|
|
|
|
233
254
|
# --li
|
|
234
255
|
break if $opts[:li].positive? && j >= $opts[:li]
|
|
235
256
|
|
|
236
|
-
|
|
257
|
+
# Use RDoc::Markup::Verbatim to save the item
|
|
258
|
+
v = RDoc::Markup::Verbatim.new
|
|
259
|
+
v.format = i
|
|
260
|
+
doc << v
|
|
237
261
|
|
|
238
262
|
# Sections
|
|
239
263
|
if $opts[:sep] && section != i[$opts[:sep]]
|
|
240
264
|
$log.debug("New section '#{$opts[:sep]}': #{i[$opts[:sep]]}")
|
|
241
265
|
|
|
242
|
-
|
|
243
|
-
v.format = { item: i, prop: $opts[:sep] }
|
|
244
|
-
doc << v
|
|
266
|
+
doc << RDoc::Markup::Raw.new($opts[:sep])
|
|
245
267
|
|
|
246
268
|
doc << RDoc::Markup::BlankLine.new if j.positive?
|
|
247
269
|
doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
|
|
@@ -255,27 +277,30 @@ items.each_with_index do |i, j|
|
|
|
255
277
|
|
|
256
278
|
# Properties
|
|
257
279
|
$opts[:props].each_with_index do |prop, k|
|
|
258
|
-
|
|
259
|
-
|
|
280
|
+
value = i[prop]
|
|
281
|
+
|
|
282
|
+
next unless value
|
|
283
|
+
next if Array === value && !value[0]
|
|
284
|
+
next if String === value && value.length == 0
|
|
260
285
|
|
|
261
|
-
$log.debug("#{prop}: #{
|
|
286
|
+
$log.debug("#{prop}: #{value}")
|
|
262
287
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
props << v
|
|
288
|
+
# Use Raw to save the property
|
|
289
|
+
props << RDoc::Markup::Raw.new(prop)
|
|
266
290
|
|
|
267
291
|
unless k.positive?
|
|
268
292
|
# First property, value only
|
|
269
|
-
props << RDoc::Markup::Heading.new(2,
|
|
293
|
+
props << RDoc::Markup::Heading.new(2, value.to_s)
|
|
270
294
|
else
|
|
271
295
|
props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
|
|
272
|
-
props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(
|
|
296
|
+
props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless(i['placeholder'])
|
|
273
297
|
end
|
|
274
298
|
end
|
|
275
299
|
|
|
276
300
|
# Print it
|
|
277
|
-
unless props.empty?
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
end
|
|
301
|
+
props << RDoc::Markup::BlankLine.new unless props.empty?
|
|
302
|
+
|
|
303
|
+
doc << props
|
|
281
304
|
end
|
|
305
|
+
|
|
306
|
+
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 =
|
|
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':
|
|
11
|
-
'red':
|
|
12
|
-
'green':
|
|
13
|
-
'yellow':
|
|
14
|
-
'blue':
|
|
15
|
-
'magenta': 35,
|
|
16
|
-
'cyan':
|
|
17
|
-
'white':
|
|
18
|
-
'default': 39,
|
|
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
|
|
130
|
-
#
|
|
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
|
|
133
|
-
# @option
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
@@ -114,6 +114,8 @@ module ICalPal
|
|
|
114
114
|
|
|
115
115
|
# Repeat for multi-day events
|
|
116
116
|
((self['duration'] / 86400).to_i + 1).times do |i|
|
|
117
|
+
break if self['sdate'] > $opts[:to]
|
|
118
|
+
|
|
117
119
|
$log.debug("multi-day event #{i + 1}") if (i > 0)
|
|
118
120
|
self['daynum'] = i + 1
|
|
119
121
|
retval.push(clone) if in_window?(self['sdate'])
|
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 +
|
|
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,51 @@ module ICalPal
|
|
|
22
24
|
when 'stores' then Store
|
|
23
25
|
when 'calendars' then Calendar
|
|
24
26
|
when 'events' then Event
|
|
25
|
-
when 'tasks' then
|
|
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::SQLException => e
|
|
62
|
+
$log.info("#{db_file}: #{e}")
|
|
63
|
+
|
|
64
|
+
rescue SQLite3::Exception => e
|
|
65
|
+
abort("#{db_file}: #{e}")
|
|
66
|
+
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
return(rows)
|
|
70
|
+
end
|
|
71
|
+
|
|
32
72
|
# @param obj [ICalPal] A +Store+ or +Calendar+
|
|
33
73
|
def initialize(obj)
|
|
34
74
|
obj['type'] = EventKit::EKSourceType.find_index { |i| i[:name] == 'Subscribed' } if obj['subcal_url']
|
|
@@ -55,6 +95,49 @@ module ICalPal
|
|
|
55
95
|
CSV::Row::new(headers, values)
|
|
56
96
|
end
|
|
57
97
|
|
|
98
|
+
# Convert +self+ to XML
|
|
99
|
+
#
|
|
100
|
+
# @return [String] All fields in a simple XML format: <field>value</field>.
|
|
101
|
+
# Fields with empty values return <field/>.
|
|
102
|
+
def to_xml
|
|
103
|
+
retval = ""
|
|
104
|
+
@self.keys.each { |k| retval += xmlify(k, @self[k]) }
|
|
105
|
+
|
|
106
|
+
retval
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Convert a key/value pair to XML. The value should be +nil+, +String+,
|
|
110
|
+
# +Integer+, +Array+, or +ICalPal::RDT+
|
|
111
|
+
#
|
|
112
|
+
# @param key The key
|
|
113
|
+
# @param value The value
|
|
114
|
+
# @return [String] The key/value pair in a simple XML format
|
|
115
|
+
def xmlify(key, value)
|
|
116
|
+
case value
|
|
117
|
+
# Nil
|
|
118
|
+
when NilClass then return("<#{key}/>")
|
|
119
|
+
|
|
120
|
+
# String, Integer
|
|
121
|
+
when String then return("<#{key}>#{value}</#{key}>")
|
|
122
|
+
when Integer then return("<#{key}>#{value}</#{key}>")
|
|
123
|
+
|
|
124
|
+
# Array
|
|
125
|
+
when Array then
|
|
126
|
+
# Treat empty arrays as nil values
|
|
127
|
+
return(xmlify(key, nil)) if value[0] == nil
|
|
128
|
+
|
|
129
|
+
retval = ""
|
|
130
|
+
value.each { |x| retval += xmlify("#{key}0", x) }
|
|
131
|
+
return("<#{key}>#{retval}</#{key}>")
|
|
132
|
+
|
|
133
|
+
# RDT
|
|
134
|
+
when ICalPal::RDT then return("<#{key}>#{value.to_s}</#{key}>")
|
|
135
|
+
|
|
136
|
+
# Unknown
|
|
137
|
+
else return("<#{key}>#{value.to_s}</#{key}>")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
58
141
|
# Get the +n+'th +dow+ in month +m+
|
|
59
142
|
#
|
|
60
143
|
# @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 =
|
|
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',
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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] = '
|
|
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
|
|
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
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:
|
|
4
|
+
version: 2.1.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-
|
|
11
|
+
date: 2024-04-28 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
|
|
@@ -65,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
65
81
|
- !ruby/object:Gem::Version
|
|
66
82
|
version: '0'
|
|
67
83
|
requirements: []
|
|
68
|
-
rubygems_version: 3.5.
|
|
84
|
+
rubygems_version: 3.5.9
|
|
69
85
|
signing_key:
|
|
70
86
|
specification_version: 4
|
|
71
87
|
summary: Command-line tool to query the macOS Calendar
|