flycal-cli 0.1.0 → 0.2.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/lib/flycal_cli/auth.rb +9 -9
- data/lib/flycal_cli/calendar_service.rb +16 -2
- data/lib/flycal_cli/cli.rb +97 -43
- data/lib/flycal_cli/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8d00cb30384c0dedb985b343cbb058db6c93b6e77469d5df0d22a6ef9c62846
|
|
4
|
+
data.tar.gz: ec29bf46d64995422a4bbf8a470035de10473bc9ceeaf2646b566347e3551d06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ed2b407b9b51840497fc68f2842703f03b99dfa283598d2dfe9b6f324484b4fe513ec5e3a66a99c672996afba1bfcfb3c362f6514eccff6db9c5fdf06773725
|
|
7
|
+
data.tar.gz: a8b666a01f5b52d59e7e63ed1bb2b8e894a860d27e16c7c63f66785fdc1da6f8e8fe3cf9ca4cfe0401c7f01124062ae8596ced033ce4d5380631a952c90c3ecd
|
data/lib/flycal_cli/auth.rb
CHANGED
|
@@ -43,9 +43,9 @@ module FlycalCli
|
|
|
43
43
|
def login
|
|
44
44
|
unless Config.credentials_exist?
|
|
45
45
|
raise FlycalCli::Error,
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
46
|
+
"Credentials file not found. Create #{Config.credentials_path} with OAuth credentials from Google Cloud Console.\n" \
|
|
47
|
+
"Go to: https://console.cloud.google.com/apis/credentials\n" \
|
|
48
|
+
"Create 'Desktop app' credentials and download the JSON as credentials.json"
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
token_store = Google::Auth::Stores::FileTokenStore.new(file: Config.tokens_path)
|
|
@@ -63,9 +63,9 @@ module FlycalCli
|
|
|
63
63
|
return creds
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
#
|
|
66
|
+
# Start local server to receive the auth code
|
|
67
67
|
code = start_redirect_server(authorizer)
|
|
68
|
-
raise FlycalCli::Error, "
|
|
68
|
+
raise FlycalCli::Error, "Authentication cancelled or failed" if code.nil? || code.empty?
|
|
69
69
|
|
|
70
70
|
authorizer.get_and_store_credentials_from_code(
|
|
71
71
|
user_id: "flycal_user",
|
|
@@ -104,12 +104,12 @@ module FlycalCli
|
|
|
104
104
|
|
|
105
105
|
if params["error"]
|
|
106
106
|
res.status = 400
|
|
107
|
-
res.body = "<h1>
|
|
107
|
+
res.body = "<h1>Error</h1><p>#{params['error']}: #{params['error_description']}</p>"
|
|
108
108
|
auth_code = nil
|
|
109
109
|
elsif params["code"]
|
|
110
110
|
auth_code = params["code"]
|
|
111
111
|
res.status = 200
|
|
112
|
-
res.body = "<h1>
|
|
112
|
+
res.body = "<h1>Authentication complete!</h1><p>You can close this window and return to the terminal.</p>"
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
Thread.new { server.shutdown }
|
|
@@ -120,9 +120,9 @@ module FlycalCli
|
|
|
120
120
|
state: state
|
|
121
121
|
)
|
|
122
122
|
|
|
123
|
-
puts "\
|
|
123
|
+
puts "\nOpen this link in your browser to authenticate:\n\n"
|
|
124
124
|
puts " #{url}\n\n"
|
|
125
|
-
puts "
|
|
125
|
+
puts "After authentication, you will be redirected back here automatically.\n\n"
|
|
126
126
|
|
|
127
127
|
server.start
|
|
128
128
|
auth_code
|
|
@@ -45,12 +45,26 @@ module FlycalCli
|
|
|
45
45
|
def list_all_events(calendar_ids, time_min:, time_max:, query: nil)
|
|
46
46
|
all_events = []
|
|
47
47
|
|
|
48
|
+
# When --description is used: fetch all events and filter client-side to guarantee
|
|
49
|
+
# contains/like behavior. We do not use the API's q param so we can ensure we
|
|
50
|
+
# return all events where the string appears in summary or description (case-insensitive).
|
|
48
51
|
calendar_ids.each do |cal_id|
|
|
49
52
|
begin
|
|
50
|
-
events = list_events(cal_id, time_min: time_min, time_max: time_max, query:
|
|
53
|
+
events = list_events(cal_id, time_min: time_min, time_max: time_max, query: nil)
|
|
51
54
|
events.each { |e| all_events << { calendar_id: cal_id, event: e } }
|
|
52
55
|
rescue Google::Apis::Errors::Error => e
|
|
53
|
-
warn "
|
|
56
|
+
warn "Error in calendar #{cal_id}: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Filter: only events where summary or description contains the search string (case-insensitive)
|
|
61
|
+
if query && !query.to_s.strip.empty?
|
|
62
|
+
q = query.strip.downcase
|
|
63
|
+
all_events.select! do |item|
|
|
64
|
+
event = item[:event]
|
|
65
|
+
summary = (event.summary || "").downcase
|
|
66
|
+
description = (event.description || "").downcase
|
|
67
|
+
summary.include?(q) || description.include?(q)
|
|
54
68
|
end
|
|
55
69
|
end
|
|
56
70
|
|
data/lib/flycal_cli/cli.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require "time"
|
|
5
|
+
require "date"
|
|
5
6
|
require "tty-prompt"
|
|
6
7
|
require "tty-spinner"
|
|
7
8
|
|
|
@@ -13,70 +14,70 @@ module FlycalCli
|
|
|
13
14
|
true
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
desc "login", "
|
|
17
|
+
desc "login", "Connect to your Google account"
|
|
17
18
|
def login
|
|
18
19
|
if Auth.logged_in?
|
|
19
|
-
puts "✓
|
|
20
|
-
puts "\
|
|
20
|
+
puts "✓ You are already connected to your Google account."
|
|
21
|
+
puts "\nRun 'flycal calendars' to set the default calendar."
|
|
21
22
|
return
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
unless Config.credentials_exist?
|
|
25
|
-
puts "
|
|
26
|
-
puts "\
|
|
27
|
-
puts "1.
|
|
28
|
-
puts "2.
|
|
29
|
-
puts "3.
|
|
30
|
-
puts "\
|
|
26
|
+
puts "Error: Credentials file not found."
|
|
27
|
+
puts "\nTo configure flycal:"
|
|
28
|
+
puts "1. Go to https://console.cloud.google.com/apis/credentials"
|
|
29
|
+
puts "2. Create 'Desktop app' credentials"
|
|
30
|
+
puts "3. Download the JSON and save it as: #{Config.credentials_path}"
|
|
31
|
+
puts "\nAlso add this URI as an authorized redirect:"
|
|
31
32
|
puts " http://127.0.0.1:9292/oauth2callback"
|
|
32
33
|
return
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
begin
|
|
36
37
|
Auth.login
|
|
37
|
-
puts "\n✓
|
|
38
|
-
puts "\
|
|
38
|
+
puts "\n✓ Authentication completed successfully!"
|
|
39
|
+
puts "\nRun 'flycal calendars' to set the default calendar."
|
|
39
40
|
rescue FlycalCli::Error => e
|
|
40
|
-
puts "
|
|
41
|
+
puts "Error: #{e.message}"
|
|
41
42
|
exit 1
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
desc "logout", "
|
|
46
|
+
desc "logout", "Disconnect from Google account"
|
|
46
47
|
def logout
|
|
47
48
|
unless Auth.logged_in?
|
|
48
|
-
puts "
|
|
49
|
+
puts "You are not connected to any Google account."
|
|
49
50
|
return
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
Auth.logout
|
|
53
|
-
puts "✓
|
|
54
|
+
puts "✓ Disconnected successfully."
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
desc "calendars", "
|
|
57
|
+
desc "calendars", "List available calendars and set the default one"
|
|
57
58
|
def calendars
|
|
58
59
|
unless Auth.logged_in?
|
|
59
|
-
puts "
|
|
60
|
+
puts "You are not connected. Run 'flycal login' first."
|
|
60
61
|
exit 1
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
creds = Auth.credentials
|
|
64
65
|
service = CalendarService.new(creds)
|
|
65
66
|
|
|
66
|
-
spinner = TTY::Spinner.new("
|
|
67
|
+
spinner = TTY::Spinner.new("Loading calendars... ", format: :dots)
|
|
67
68
|
spinner.auto_spin
|
|
68
69
|
calendars = service.list_calendars
|
|
69
70
|
spinner.stop("✓")
|
|
70
71
|
|
|
71
72
|
if calendars.empty?
|
|
72
|
-
puts "
|
|
73
|
+
puts "No calendars found."
|
|
73
74
|
return
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
#
|
|
77
|
+
# Build selection list (display => calendar_id)
|
|
77
78
|
default_id = Config.calendar_default
|
|
78
79
|
choices = calendars.to_h do |cal|
|
|
79
|
-
primary = cal.primary ? " (
|
|
80
|
+
primary = cal.primary ? " (primary)" : ""
|
|
80
81
|
default = (cal.id == default_id) ? " [default]" : ""
|
|
81
82
|
summary = cal.summary || cal.id
|
|
82
83
|
label = "#{summary}#{primary}#{default}"
|
|
@@ -85,7 +86,7 @@ module FlycalCli
|
|
|
85
86
|
|
|
86
87
|
prompt = TTY::Prompt.new
|
|
87
88
|
selected_id = prompt.select(
|
|
88
|
-
"
|
|
89
|
+
"Choose the default calendar:",
|
|
89
90
|
choices,
|
|
90
91
|
per_page: 15,
|
|
91
92
|
filter: true
|
|
@@ -94,26 +95,52 @@ module FlycalCli
|
|
|
94
95
|
calendar = calendars.find { |c| c.id == selected_id }
|
|
95
96
|
if calendar
|
|
96
97
|
Config.calendar_default = calendar.id
|
|
97
|
-
puts "\n✓
|
|
98
|
+
puts "\n✓ Default calendar set to: #{calendar.summary}"
|
|
98
99
|
end
|
|
99
100
|
end
|
|
100
101
|
|
|
101
|
-
desc "search", "
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
desc "search", "Search for events in calendars"
|
|
103
|
+
long_desc <<-LONGDESC
|
|
104
|
+
Search for events in your Google calendar(s).
|
|
105
|
+
|
|
106
|
+
Time range:
|
|
107
|
+
-f, --from DATE Start (default: today midnight)
|
|
108
|
+
-t, --to DATE End (default: 23:59 of day 30)
|
|
109
|
+
-i, --in DURATION Duration from --from, overrides --to.
|
|
110
|
+
Format: 30days, 48hours, 2months, 1year (no space).
|
|
111
|
+
With space use quotes: --in "30 days"
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
flycal search
|
|
115
|
+
flycal search --in 30days -d meeting
|
|
116
|
+
flycal search -i 1months --description rappydrive
|
|
117
|
+
flycal search -f 2025-03-01 --in 2months
|
|
118
|
+
LONGDESC
|
|
119
|
+
option :calendar, type: :string, aliases: "-c", desc: "Calendar name or ID"
|
|
120
|
+
option :from, type: :string, aliases: "-f", desc: "Start (default: today midnight)"
|
|
121
|
+
option :to, type: :string, aliases: "-t", desc: "End (default: 23:59 of day 30)"
|
|
122
|
+
option :in, type: :string, aliases: "-i", desc: "Duration: 30days, 48hours, 2months, 1year (overrides --to)"
|
|
123
|
+
option :description, type: :string, aliases: "-d", desc: "Filter by text in event"
|
|
106
124
|
def search
|
|
107
125
|
unless Auth.logged_in?
|
|
108
|
-
puts "
|
|
126
|
+
puts "You are not connected. Run 'flycal login' first."
|
|
109
127
|
exit 1
|
|
110
128
|
end
|
|
111
129
|
|
|
112
|
-
time_min = parse_datetime(options[:from])
|
|
113
|
-
|
|
130
|
+
time_min = options[:from].to_s.empty? ? Date.today.to_time : parse_datetime(options[:from])
|
|
131
|
+
begin
|
|
132
|
+
time_max = if options[:in].to_s.empty?
|
|
133
|
+
options[:to].to_s.empty? ? (Date.today + 30).to_time + 86400 - 1 : parse_datetime(options[:to], end_of_day: true)
|
|
134
|
+
else
|
|
135
|
+
parse_duration_in(options[:in], time_min)
|
|
136
|
+
end
|
|
137
|
+
rescue FlycalCli::Error => e
|
|
138
|
+
puts "Error: #{e.message}"
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
|
114
141
|
|
|
115
142
|
if time_min > time_max
|
|
116
|
-
puts "
|
|
143
|
+
puts "Error: 'from' date must be before 'to' date."
|
|
117
144
|
exit 1
|
|
118
145
|
end
|
|
119
146
|
|
|
@@ -122,7 +149,7 @@ module FlycalCli
|
|
|
122
149
|
|
|
123
150
|
calendar_ids = resolve_calendar_ids(service, options[:calendar])
|
|
124
151
|
if calendar_ids.empty?
|
|
125
|
-
puts "
|
|
152
|
+
puts "No calendars found."
|
|
126
153
|
exit 1
|
|
127
154
|
end
|
|
128
155
|
|
|
@@ -134,7 +161,7 @@ module FlycalCli
|
|
|
134
161
|
)
|
|
135
162
|
|
|
136
163
|
print_events(service, events)
|
|
137
|
-
print_summary(events)
|
|
164
|
+
print_summary(events, time_min: time_min, time_max: time_max)
|
|
138
165
|
end
|
|
139
166
|
|
|
140
167
|
default_task :help
|
|
@@ -149,7 +176,29 @@ module FlycalCli
|
|
|
149
176
|
else
|
|
150
177
|
d = Date.parse(str)
|
|
151
178
|
t = d.to_time
|
|
152
|
-
end_of_day ? t + 86400 - 1 : t # 23:59:59
|
|
179
|
+
end_of_day ? t + 86400 - 1 : t # 23:59:59 for --to when date-only
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def parse_duration_in(str, from_time)
|
|
184
|
+
# Support both "30days" and "30 days" (no space avoids shell splitting)
|
|
185
|
+
m = str.to_s.strip.match(/\A(\d+)\s*(day|days|d|hour|hours|h|month|months|m|year|years|y)\z/i)
|
|
186
|
+
raise FlycalCli::Error, "Invalid --in format. Use: 30days, 48hours, 2months, 1year (no space, or quote: --in \"30 days\")" unless m
|
|
187
|
+
|
|
188
|
+
n = m[1].to_i
|
|
189
|
+
unit = m[2].downcase
|
|
190
|
+
|
|
191
|
+
case unit
|
|
192
|
+
when "hour", "hours", "h"
|
|
193
|
+
from_time + n * 3600
|
|
194
|
+
when "day", "days", "d"
|
|
195
|
+
from_time + n * 86400
|
|
196
|
+
when "month", "months", "m"
|
|
197
|
+
(from_time.to_date >> n).to_time + (from_time.hour * 3600 + from_time.min * 60 + from_time.sec)
|
|
198
|
+
when "year", "years", "y"
|
|
199
|
+
(from_time.to_date >> (n * 12)).to_time + (from_time.hour * 3600 + from_time.min * 60 + from_time.sec)
|
|
200
|
+
else
|
|
201
|
+
raise FlycalCli::Error, "Invalid unit: #{unit}. Use: days, hours, months, years"
|
|
153
202
|
end
|
|
154
203
|
end
|
|
155
204
|
|
|
@@ -161,15 +210,15 @@ module FlycalCli
|
|
|
161
210
|
if default_id
|
|
162
211
|
return [default_id]
|
|
163
212
|
end
|
|
164
|
-
#
|
|
213
|
+
# Use primary calendar if available
|
|
165
214
|
primary = calendar_lists.find(&:primary)
|
|
166
215
|
return [primary.id] if primary
|
|
167
|
-
#
|
|
216
|
+
# Otherwise first calendar
|
|
168
217
|
return [calendar_lists.first.id] if calendar_lists.any?
|
|
169
218
|
return []
|
|
170
219
|
end
|
|
171
220
|
|
|
172
|
-
#
|
|
221
|
+
# Search by name or ID
|
|
173
222
|
matches = calendar_lists.select do |cal|
|
|
174
223
|
cal.id == calendar_name ||
|
|
175
224
|
(cal.summary && cal.summary.downcase.include?(calendar_name.downcase))
|
|
@@ -179,7 +228,7 @@ module FlycalCli
|
|
|
179
228
|
end
|
|
180
229
|
|
|
181
230
|
def print_events(service, events)
|
|
182
|
-
#
|
|
231
|
+
# Use calendar list for names (avoids extra API calls)
|
|
183
232
|
calendar_list = service.list_calendars
|
|
184
233
|
calendar_names = calendar_list.to_h { |c| [c.id, c.summary || c.id] }
|
|
185
234
|
|
|
@@ -193,7 +242,7 @@ module FlycalCli
|
|
|
193
242
|
|
|
194
243
|
start_str = format_datetime(start_time)
|
|
195
244
|
end_str = format_datetime(end_time)
|
|
196
|
-
desc = event.summary || "(
|
|
245
|
+
desc = event.summary || "(no title)"
|
|
197
246
|
desc = event.description&.slice(0, 80) if event.summary.nil? && event.description
|
|
198
247
|
|
|
199
248
|
puts "#{cal_name} | #{start_str} | #{end_str} | #{desc}"
|
|
@@ -210,7 +259,7 @@ module FlycalCli
|
|
|
210
259
|
end
|
|
211
260
|
end
|
|
212
261
|
|
|
213
|
-
def print_summary(events)
|
|
262
|
+
def print_summary(events, time_min:, time_max:)
|
|
214
263
|
total_minutes = 0
|
|
215
264
|
|
|
216
265
|
events.each do |item|
|
|
@@ -227,10 +276,15 @@ module FlycalCli
|
|
|
227
276
|
|
|
228
277
|
hours = (total_minutes / 60).floor
|
|
229
278
|
mins = (total_minutes % 60).round
|
|
279
|
+
total_working_days = (total_minutes / 60.0 / 8).round(1)
|
|
280
|
+
|
|
281
|
+
from_str = time_min.strftime("%Y-%m-%d %H:%M")
|
|
282
|
+
to_str = time_max.strftime("%Y-%m-%d %H:%M")
|
|
230
283
|
|
|
231
284
|
puts "\n---"
|
|
232
|
-
puts "
|
|
233
|
-
puts "
|
|
285
|
+
puts "From: #{from_str} | To: #{to_str}"
|
|
286
|
+
puts "Events found: #{events.size}"
|
|
287
|
+
puts "Total time occupied: #{hours}h #{mins}min (#{total_working_days} working days)"
|
|
234
288
|
end
|
|
235
289
|
end
|
|
236
290
|
end
|
data/lib/flycal_cli/version.rb
CHANGED