r_cal 0.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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +239 -0
  4. data/LICENSE +21 -0
  5. data/README.md +174 -0
  6. data/Rakefile +47 -0
  7. data/dev.yml +3 -0
  8. data/exe/rcal +17 -0
  9. data/lib/rcal/adapters/auth/base.rb +27 -0
  10. data/lib/rcal/adapters/auth/google.rb +71 -0
  11. data/lib/rcal/adapters/calendar/base.rb +35 -0
  12. data/lib/rcal/adapters/calendar/google.rb +161 -0
  13. data/lib/rcal/adapters/date_parser/base.rb +11 -0
  14. data/lib/rcal/adapters/date_parser/chronic.rb +86 -0
  15. data/lib/rcal/adapters/duration_parser/base.rb +15 -0
  16. data/lib/rcal/adapters/duration_parser/chronic_duration.rb +47 -0
  17. data/lib/rcal/adapters/ics_parser/base.rb +15 -0
  18. data/lib/rcal/adapters/ics_parser/icalendar.rb +68 -0
  19. data/lib/rcal/auth.rb +46 -0
  20. data/lib/rcal/calendar_service.rb +103 -0
  21. data/lib/rcal/command.rb +33 -0
  22. data/lib/rcal/commands/add.rb +147 -0
  23. data/lib/rcal/commands/agenda.rb +143 -0
  24. data/lib/rcal/commands/edit.rb +158 -0
  25. data/lib/rcal/commands/help.rb +21 -0
  26. data/lib/rcal/commands/import.rb +121 -0
  27. data/lib/rcal/commands/init.rb +158 -0
  28. data/lib/rcal/commands/list.rb +49 -0
  29. data/lib/rcal/commands/quick.rb +80 -0
  30. data/lib/rcal/commands.rb +21 -0
  31. data/lib/rcal/config.rb +68 -0
  32. data/lib/rcal/date_parser.rb +27 -0
  33. data/lib/rcal/duration_parser.rb +31 -0
  34. data/lib/rcal/entry_point.rb +10 -0
  35. data/lib/rcal/errors.rb +7 -0
  36. data/lib/rcal/formatters/agenda.rb +118 -0
  37. data/lib/rcal/ics_parser.rb +31 -0
  38. data/lib/rcal/models/calendar.rb +61 -0
  39. data/lib/rcal/models/event.rb +140 -0
  40. data/lib/rcal/predicate_collection.rb +65 -0
  41. data/lib/rcal/presenters/calendar_presenter.rb +34 -0
  42. data/lib/rcal/presenters/event_presenter.rb +98 -0
  43. data/lib/rcal/version.rb +5 -0
  44. data/lib/rcal.rb +27 -0
  45. data/mise.toml +2 -0
  46. metadata +328 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 94b9764871e9a1c0265ddba97c60df40b405b8123d8ed948161e5ce282f11047
4
+ data.tar.gz: 8be2bb3df5dae0b2df5aba144076f938358817bbb9a4056756a77e9b4f59c48f
5
+ SHA512:
6
+ metadata.gz: a93938c290893283cccaa9e9cf3221abeb44711752faae4d080bccaf6338dfbdeaecb4ebe2f22e7464cb268b782dd3cef2edb77b7e3e4057552a6beb6300657f
7
+ data.tar.gz: 855962fb33a37677c0c42a7dba07ec3ed6473da9bf9d31b8024abc16d49e5d46c1e73e43239b530988ac27143a905b295b0b00e5c6315d5f0dc4ee675dc1c07d
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,239 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rcal (0.1.0)
5
+ chronic (~> 0.10)
6
+ chronic_duration (~> 0.10)
7
+ cli-kit (~> 5.2.0)
8
+ cli-ui (~> 2.7.0)
9
+ google-apis-calendar_v3 (~> 0.30)
10
+ googleauth (~> 1.8)
11
+ icalendar (~> 2.10)
12
+ launchy (~> 2.4)
13
+ pstore
14
+ readline
15
+ reline
16
+
17
+ GEM
18
+ remote: https://rubygems.org/
19
+ specs:
20
+ addressable (2.8.8)
21
+ public_suffix (>= 2.0.2, < 8.0)
22
+ ansi (1.5.0)
23
+ ast (2.4.3)
24
+ base64 (0.3.0)
25
+ builder (3.3.0)
26
+ chronic (0.10.2)
27
+ chronic_duration (0.10.6)
28
+ numerizer (~> 0.1.1)
29
+ cli-kit (5.2.0)
30
+ cli-ui (~> 2.4)
31
+ cli-ui (2.7.0)
32
+ declarative (0.0.20)
33
+ faraday (2.14.0)
34
+ faraday-net_http (>= 2.0, < 3.5)
35
+ json
36
+ logger
37
+ faraday-follow_redirects (0.5.0)
38
+ faraday (>= 1, < 3)
39
+ faraday-net_http (3.4.2)
40
+ net-http (~> 0.5)
41
+ google-apis-calendar_v3 (0.51.0)
42
+ google-apis-core (>= 0.15.0, < 2.a)
43
+ google-apis-core (1.0.2)
44
+ addressable (~> 2.8, >= 2.8.7)
45
+ faraday (~> 2.13)
46
+ faraday-follow_redirects (~> 0.3)
47
+ googleauth (~> 1.14)
48
+ mini_mime (~> 1.1)
49
+ representable (~> 3.0)
50
+ retriable (~> 3.1)
51
+ google-cloud-env (2.3.1)
52
+ base64 (~> 0.2)
53
+ faraday (>= 1.0, < 3.a)
54
+ google-logging-utils (0.2.0)
55
+ googleauth (1.16.1)
56
+ faraday (>= 1.0, < 3.a)
57
+ google-cloud-env (~> 2.2)
58
+ google-logging-utils (~> 0.1)
59
+ jwt (>= 1.4, < 4.0)
60
+ multi_json (~> 1.11)
61
+ os (>= 0.9, < 2.0)
62
+ signet (>= 0.16, < 2.a)
63
+ icalendar (2.12.1)
64
+ base64
65
+ ice_cube (~> 0.16)
66
+ logger
67
+ ostruct
68
+ ice_cube (0.17.0)
69
+ io-console (0.8.2)
70
+ json (2.18.0)
71
+ jwt (3.1.2)
72
+ base64
73
+ language_server-protocol (3.17.0.5)
74
+ launchy (2.5.2)
75
+ addressable (~> 2.8)
76
+ lint_roller (1.1.0)
77
+ logger (1.7.0)
78
+ mini_mime (1.1.5)
79
+ minitest (5.27.0)
80
+ minitest-reporters (1.7.1)
81
+ ansi
82
+ builder
83
+ minitest (>= 5.0)
84
+ ruby-progressbar
85
+ mocha (2.8.2)
86
+ ruby2_keywords (>= 0.0.5)
87
+ multi_json (1.19.1)
88
+ net-http (0.9.1)
89
+ uri (>= 0.11.1)
90
+ numerizer (0.1.1)
91
+ os (1.1.4)
92
+ ostruct (0.6.3)
93
+ parallel (1.27.0)
94
+ parser (3.3.10.1)
95
+ ast (~> 2.4.1)
96
+ racc
97
+ prism (1.9.0)
98
+ pstore (0.2.0)
99
+ public_suffix (7.0.2)
100
+ racc (1.8.1)
101
+ rainbow (3.1.1)
102
+ rake (13.3.1)
103
+ readline (0.0.4)
104
+ reline
105
+ regexp_parser (2.11.3)
106
+ reline (0.6.3)
107
+ io-console (~> 0.5)
108
+ representable (3.2.0)
109
+ declarative (< 0.1.0)
110
+ trailblazer-option (>= 0.1.1, < 0.2.0)
111
+ uber (< 0.2.0)
112
+ retriable (3.1.2)
113
+ rubocop (1.82.1)
114
+ json (~> 2.3)
115
+ language_server-protocol (~> 3.17.0.2)
116
+ lint_roller (~> 1.1.0)
117
+ parallel (~> 1.10)
118
+ parser (>= 3.3.0.2)
119
+ rainbow (>= 2.2.2, < 4.0)
120
+ regexp_parser (>= 2.9.3, < 3.0)
121
+ rubocop-ast (>= 1.48.0, < 2.0)
122
+ ruby-progressbar (~> 1.7)
123
+ unicode-display_width (>= 2.4.0, < 4.0)
124
+ rubocop-ast (1.49.0)
125
+ parser (>= 3.3.7.2)
126
+ prism (~> 1.7)
127
+ rubocop-performance (1.26.1)
128
+ lint_roller (~> 1.1)
129
+ rubocop (>= 1.75.0, < 2.0)
130
+ rubocop-ast (>= 1.47.1, < 2.0)
131
+ ruby-progressbar (1.13.0)
132
+ ruby2_keywords (0.0.5)
133
+ signet (0.21.0)
134
+ addressable (~> 2.8)
135
+ faraday (>= 0.17.5, < 3.a)
136
+ jwt (>= 1.5, < 4.0)
137
+ multi_json (~> 1.10)
138
+ standard (1.53.0)
139
+ language_server-protocol (~> 3.17.0.2)
140
+ lint_roller (~> 1.0)
141
+ rubocop (~> 1.82.0)
142
+ standard-custom (~> 1.0.0)
143
+ standard-performance (~> 1.8)
144
+ standard-custom (1.0.2)
145
+ lint_roller (~> 1.0)
146
+ rubocop (~> 1.50)
147
+ standard-performance (1.9.0)
148
+ lint_roller (~> 1.1)
149
+ rubocop-performance (~> 1.26.0)
150
+ timecop (0.9.10)
151
+ trailblazer-option (0.1.2)
152
+ uber (0.1.0)
153
+ unicode-display_width (3.2.0)
154
+ unicode-emoji (~> 4.1)
155
+ unicode-emoji (4.2.0)
156
+ uri (1.1.1)
157
+
158
+ PLATFORMS
159
+ arm64-darwin-25
160
+ ruby
161
+
162
+ DEPENDENCIES
163
+ minitest (~> 5.20)
164
+ minitest-reporters
165
+ mocha (~> 2.1)
166
+ rake
167
+ rcal!
168
+ standard
169
+ timecop (~> 0.9)
170
+
171
+ CHECKSUMS
172
+ addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057
173
+ ansi (1.5.0) sha256=5408253274e33d9d27d4a98c46d2998266fd51cba58a7eb9d08f50e57ed23592
174
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
175
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
176
+ builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
177
+ chronic (0.10.2) sha256=766f2fcce6ac3cc152249ed0f2b827770d3e517e2e87c5fba7ed74f4889d2dc3
178
+ chronic_duration (0.10.6) sha256=fac58d4147d3183a40811400380cafcef049f2bb02421d2fd1c6e685fbe8facc
179
+ cli-kit (5.2.0) sha256=6eb44b6648fb644e801c8d05cbc85e4a7ea5c5fc2b42d2312e2ea78606a3be7d
180
+ cli-ui (2.7.0) sha256=e36147f2e8088796b996f0560f577bd1599f2b6aa3c374fe09fff59af5589c35
181
+ declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9
182
+ faraday (2.14.0) sha256=8699cfe5d97e55268f2596f9a9d5a43736808a943714e3d9a53e6110593941cd
183
+ faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad
184
+ faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c
185
+ google-apis-calendar_v3 (0.51.0) sha256=ed2647ee7d58e79c5e510935d012396de6f6649a435359a4380d58d3b13fce05
186
+ google-apis-core (1.0.2) sha256=ba4579aaadc902d6cc7bc8db88f566ab00f5e31ea87ab41e9f9a032c470f2629
187
+ google-cloud-env (2.3.1) sha256=0faac01eb27be78c2591d64433663b1a114f8f7af55a4f819755426cac9178e7
188
+ google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b
189
+ googleauth (1.16.1) sha256=36776bce9d55d8c1a0c6638c939b000dcee5954ca5b728f06ec4c2df4a46709c
190
+ icalendar (2.12.1) sha256=ecff56c550aed551f29ad1faad0da54bf62362dfaf22a428bd7ad782938fe764
191
+ ice_cube (0.17.0) sha256=32deb45dda4b4acc53505c2f581f6d32b5afc04d29b9004769944a0df5a5fcbe
192
+ io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
193
+ json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
194
+ jwt (3.1.2) sha256=af6991f19a6bb4060d618d9add7a66f0eeb005ac0bc017cd01f63b42e122d535
195
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
196
+ launchy (2.5.2) sha256=8aa0441655aec5514008e1d04892c2de3ba57bd337afb984568da091121a241b
197
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
198
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
199
+ mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
200
+ minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
201
+ minitest-reporters (1.7.1) sha256=5060413a0c95b8c32fe73e0606f3631c173a884d7900e50013e15094eb50562c
202
+ mocha (2.8.2) sha256=1f77e729db47e72b4ef776461ce20caeec2572ffdf23365b0a03608fee8f4eee
203
+ multi_json (1.19.1) sha256=7aefeff8f2c854bf739931a238e4aea64592845e0c0395c8a7d2eea7fdd631b7
204
+ net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
205
+ numerizer (0.1.1) sha256=10ec9efec62472b69a3a0e275a18a44baa595ce6e2e4cfc1678d5cb2974c336f
206
+ os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f
207
+ ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
208
+ parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
209
+ parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
210
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
211
+ pstore (0.2.0) sha256=d6e5c7e8e22392235e88bbe82959059ba768a797b5bd0ebf5ac80a3311ce74a8
212
+ public_suffix (7.0.2) sha256=9114090c8e4e7135c1fd0e7acfea33afaab38101884320c65aaa0ffb8e26a857
213
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
214
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
215
+ rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
216
+ rcal (0.1.0)
217
+ readline (0.0.4) sha256=6138eef17be2b98298b672c3ea63bf9cb5158d401324f26e1e84f235879c1d6a
218
+ regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
219
+ reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
220
+ representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace
221
+ retriable (3.1.2) sha256=0a5a5d0ca4ba61a76fb31a17ab8f7f80281beb040c329d34dfc137a1398688e0
222
+ rubocop (1.82.1) sha256=09f1a6a654a960eda767aebea33e47603080f8e9c9a3f019bf9b94c9cab5e273
223
+ rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
224
+ rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
225
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
226
+ ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
227
+ signet (0.21.0) sha256=d617e9fbf24928280d39dcfefba9a0372d1c38187ffffd0a9283957a10a8cd5b
228
+ standard (1.53.0) sha256=f3c9493385db7079d0abce6f7582f553122156997b81258cd361d3480eeacf9c
229
+ standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b
230
+ standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2
231
+ timecop (0.9.10) sha256=12ba45ce57cdcf6b1043cb6cdffa6381fd89ce10d369c28a7f6f04dc1b0cd8eb
232
+ trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3
233
+ uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc
234
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
235
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
236
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
237
+
238
+ BUNDLED WITH
239
+ 4.0.3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Drew Bragg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # rcal
2
+
3
+ A fast, pure Ruby command-line interface for Google Calendar.
4
+
5
+ ## Features
6
+
7
+ - **Natural language dates** - "tomorrow 3pm", "next monday", "friday noon"
8
+ - **Event management** - create, edit, and view events
9
+ - **ICS import** - import calendar invites from .ics files
10
+ - **Predicate filtering** - filter by recurring, one-on-one, declined, etc.
11
+ - **Multiple calendars** - work with any calendar in your account
12
+ - **Fast startup** - built with cli-kit for snappy performance
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ git clone https://github.com/yourusername/rcal.git
18
+ cd rcal
19
+ bundle install
20
+ ```
21
+
22
+ ## Setup
23
+
24
+ 1. Create OAuth credentials in [Google Cloud Console](https://console.cloud.google.com/apis/credentials):
25
+ - Create a new project (or use existing)
26
+ - Enable the Google Calendar API
27
+ - Create OAuth 2.0 Client ID (Desktop app type)
28
+ - Note your Client ID and Client Secret
29
+
30
+ 2. Authenticate rcal:
31
+
32
+ ```bash
33
+ rcal init --client-id=YOUR_CLIENT_ID --client-secret=YOUR_CLIENT_SECRET
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### View Events
39
+
40
+ ```bash
41
+ # Today's agenda
42
+ rcal agenda
43
+
44
+ # Tomorrow
45
+ rcal agenda tomorrow
46
+
47
+ # Next 7 days
48
+ rcal agenda --days=7
49
+
50
+ # Specific date range
51
+ rcal agenda "next monday" "next friday"
52
+
53
+ # Filter by predicates
54
+ rcal agenda --must-be=recurring
55
+ rcal agenda --must-not-be=declined,all_day
56
+
57
+ # Show event IDs (for editing)
58
+ rcal agenda --show-ids
59
+ ```
60
+
61
+ ### Create Events
62
+
63
+ ```bash
64
+ # Quick add with natural language
65
+ rcal quick "Lunch with Sarah tomorrow noon"
66
+ rcal quick "Team standup every weekday 10am"
67
+
68
+ # Structured add
69
+ rcal add --title="Team Meeting" --when="tomorrow 3pm"
70
+ rcal add --title="Lunch" --when="friday noon" --duration=1h30m --location="Cafe"
71
+ rcal add --title="Vacation" --when="monday" --all-day
72
+ ```
73
+
74
+ ### Edit Events
75
+
76
+ ```bash
77
+ # Get event ID from agenda
78
+ rcal agenda --show-ids
79
+
80
+ # Edit event
81
+ rcal edit EVENT_ID --title="Updated Title"
82
+ rcal edit EVENT_ID --when="tomorrow 4pm" --duration=30m
83
+ rcal edit EVENT_ID --location="Room 202"
84
+ ```
85
+
86
+ ### Import ICS Files
87
+
88
+ ```bash
89
+ # Import calendar invite
90
+ rcal import ~/Downloads/invite.ics
91
+
92
+ # Import to specific calendar
93
+ rcal import meeting.ics --calendar=work@company.com
94
+ ```
95
+
96
+ ### List Calendars
97
+
98
+ ```bash
99
+ rcal list
100
+ ```
101
+
102
+ ## Available Predicates
103
+
104
+ Use with `--must-be` and `--must-not-be` flags:
105
+
106
+ | Predicate | Description |
107
+ |-----------|-------------|
108
+ | `accepted` | Events you've accepted |
109
+ | `declined` | Events you've declined |
110
+ | `awaiting` | Events awaiting your response |
111
+ | `tentative` | Events you've tentatively accepted |
112
+ | `all_day` | All-day events |
113
+ | `recurring` | Recurring events |
114
+ | `one_on_one` | Events with exactly 2 attendees |
115
+ | `busy` | Events that block your calendar |
116
+ | `commitment` | Events with 2+ attendees you haven't declined |
117
+
118
+ ## Duration Formats
119
+
120
+ - `30m` - 30 minutes
121
+ - `1h` - 1 hour
122
+ - `1h30m` - 1 hour 30 minutes
123
+ - `1.5h` - 1.5 hours (90 minutes)
124
+ - `2h15m` - 2 hours 15 minutes
125
+
126
+ ## Configuration
127
+
128
+ rcal follows XDG Base Directory specification:
129
+
130
+ - Config: `~/.config/rcal/`
131
+ - Data: `~/.local/share/rcal/`
132
+
133
+ ## Development
134
+
135
+ ```bash
136
+ # Run all tests and linter
137
+ bundle exec rake
138
+
139
+ # Run all tests
140
+ bundle exec rake test
141
+
142
+ # Run unit tests only
143
+ bundle exec rake test:unit
144
+
145
+ # Run linter
146
+ bundle exec rake lint
147
+
148
+ # Run linter with auto-fix
149
+ bundle exec rake lint:fix
150
+
151
+ # List all available tasks
152
+ bundle exec rake -T
153
+
154
+ # Run CLI locally
155
+ ./exe/rcal help
156
+ ```
157
+
158
+ ## Architecture
159
+
160
+ rcal uses an adapter pattern for all external dependencies:
161
+
162
+ ```
163
+ Rcal::DateParser -> Rcal::Adapters::DateParser::Chronic
164
+ Rcal::DurationParser -> Rcal::Adapters::DurationParser::ChronicDuration
165
+ Rcal::IcsParser -> Rcal::Adapters::IcsParser::Icalendar
166
+ Rcal::CalendarService -> Rcal::Adapters::Calendar::Google
167
+ Rcal::Auth -> Rcal::Adapters::Auth::Google
168
+ ```
169
+
170
+ This allows swapping implementations without changing application code.
171
+
172
+ ## License
173
+
174
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/testtask"
4
+ require "standard/rake"
5
+
6
+ #
7
+ # Test tasks
8
+ #
9
+ desc "Run all tests"
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << "test"
12
+ t.libs << "lib"
13
+ t.test_files = FileList["test/**/*_test.rb"]
14
+ end
15
+
16
+ desc "Run unit tests only"
17
+ Rake::TestTask.new("test:unit") do |t|
18
+ t.libs << "test"
19
+ t.libs << "lib"
20
+ t.test_files = FileList["test/unit/**/*_test.rb"]
21
+ end
22
+
23
+ desc "Run integration tests only"
24
+ Rake::TestTask.new("test:integration") do |t|
25
+ t.libs << "test"
26
+ t.libs << "lib"
27
+ t.test_files = FileList["test/integration/**/*_test.rb"]
28
+ end
29
+
30
+ #
31
+ # Lint tasks
32
+ #
33
+ desc "Run StandardRB linter"
34
+ task :lint do
35
+ sh "bundle exec standardrb"
36
+ end
37
+
38
+ desc "Run StandardRB linter and auto-fix"
39
+ task "lint:fix" do
40
+ sh "bundle exec standardrb --fix"
41
+ end
42
+
43
+ #
44
+ # Default task
45
+ #
46
+ desc "Run tests and linter"
47
+ task default: [:test, :lint]
data/dev.yml ADDED
@@ -0,0 +1,3 @@
1
+ up:
2
+ - ruby: 4.0.1
3
+ - bundler
data/exe/rcal ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ Encoding.default_external = Encoding::UTF_8
4
+ Encoding.default_internal = Encoding::UTF_8
5
+
6
+ unshift_path = ->(path) {
7
+ p = File.expand_path("../../#{path}", __FILE__)
8
+ $LOAD_PATH.unshift(p) unless $LOAD_PATH.include?(p)
9
+ }
10
+ unshift_path.call("lib")
11
+
12
+ require "bundler/setup"
13
+ require "rcal"
14
+
15
+ exit(Rcal::ErrorHandler.call do
16
+ Rcal::EntryPoint.call(ARGV.dup)
17
+ end)
@@ -0,0 +1,27 @@
1
+ module Rcal
2
+ module Adapters
3
+ module Auth
4
+ class Base
5
+ def store_credentials(credentials)
6
+ raise NotImplementedError, "#{self.class} must implement #store_credentials"
7
+ end
8
+
9
+ def load_credentials
10
+ raise NotImplementedError, "#{self.class} must implement #load_credentials"
11
+ end
12
+
13
+ def authenticated?
14
+ raise NotImplementedError, "#{self.class} must implement #authenticated?"
15
+ end
16
+
17
+ def token_expired?
18
+ raise NotImplementedError, "#{self.class} must implement #token_expired?"
19
+ end
20
+
21
+ def clear_credentials
22
+ raise NotImplementedError, "#{self.class} must implement #clear_credentials"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,71 @@
1
+ require "json"
2
+ require "fileutils"
3
+ require_relative "base"
4
+
5
+ module Rcal
6
+ module Adapters
7
+ module Auth
8
+ class Google < Base
9
+ def self.default_token_path
10
+ base = ENV.fetch("XDG_DATA_HOME") { File.join(ENV.fetch("HOME"), ".local", "share") }
11
+ File.join(base, "rcal", "tokens.json")
12
+ end
13
+
14
+ def initialize(token_path: self.class.default_token_path)
15
+ @token_path = token_path
16
+ end
17
+
18
+ def store_credentials(credentials)
19
+ ensure_directory_exists
20
+
21
+ token_data = {
22
+ "access_token" => credentials.access_token,
23
+ "refresh_token" => credentials.refresh_token,
24
+ "expires_at" => credentials.expires_at.to_i
25
+ }
26
+
27
+ File.write(@token_path, JSON.generate(token_data))
28
+ File.chmod(0o600, @token_path)
29
+ end
30
+
31
+ def load_credentials
32
+ return nil unless File.exist?(@token_path)
33
+
34
+ JSON.parse(File.read(@token_path))
35
+ rescue JSON::ParserError
36
+ nil
37
+ end
38
+
39
+ def authenticated?
40
+ creds = load_credentials
41
+ return false if creds.nil?
42
+
43
+ # Need at least a refresh token to be considered authenticated
44
+ # (we can refresh expired access tokens)
45
+ !creds["refresh_token"].nil? && !creds["refresh_token"].empty?
46
+ end
47
+
48
+ def token_expired?
49
+ creds = load_credentials
50
+ return true if creds.nil?
51
+
52
+ expires_at = creds["expires_at"]
53
+ return true if expires_at.nil?
54
+
55
+ Time.at(expires_at) <= Time.now
56
+ end
57
+
58
+ def clear_credentials
59
+ FileUtils.rm_f(@token_path)
60
+ end
61
+
62
+ private
63
+
64
+ def ensure_directory_exists
65
+ dir = File.dirname(@token_path)
66
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,35 @@
1
+ module Rcal
2
+ module Adapters
3
+ module Calendar
4
+ class Base
5
+ def list_calendars
6
+ raise NotImplementedError, "#{self.class} must implement #list_calendars"
7
+ end
8
+
9
+ def list_events(calendar_id:, time_min:, time_max:)
10
+ raise NotImplementedError, "#{self.class} must implement #list_events"
11
+ end
12
+
13
+ def get_event(calendar_id:, event_id:)
14
+ raise NotImplementedError, "#{self.class} must implement #get_event"
15
+ end
16
+
17
+ def create_event(calendar_id:, event:)
18
+ raise NotImplementedError, "#{self.class} must implement #create_event"
19
+ end
20
+
21
+ def update_event(calendar_id:, event_id:, event:)
22
+ raise NotImplementedError, "#{self.class} must implement #update_event"
23
+ end
24
+
25
+ def delete_event(calendar_id:, event_id:)
26
+ raise NotImplementedError, "#{self.class} must implement #delete_event"
27
+ end
28
+
29
+ def quick_add(calendar_id:, text:)
30
+ raise NotImplementedError, "#{self.class} must implement #quick_add"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end