flycal-cli 0.4.1 → 0.6.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 +76 -17
- data/config/defaults.yml +8 -2
- data/lib/flycal_cli/cli.rb +270 -102
- data/lib/flycal_cli/config.rb +46 -1
- data/lib/flycal_cli/date_time_parser.rb +129 -0
- data/lib/flycal_cli/duration_parser.rb +15 -6
- data/lib/flycal_cli/locale.rb +6 -0
- data/lib/flycal_cli/slot_finder.rb +30 -12
- data/lib/flycal_cli/slot_formatter.rb +25 -0
- data/lib/flycal_cli/version.rb +1 -1
- data/lib/flycal_cli.rb +1 -0
- data/locales/en.json +26 -2
- data/locales/it.json +26 -2
- data/mcp/README.md +29 -0
- data/mcp/tools.json +318 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3dde0dd31d735707a1fc4e9f1e125c2f8a189749e3a3458ba3ff5e54eb9b47b
|
|
4
|
+
data.tar.gz: edc5fcb47573491a2b23ca784bd263a6f920c60cbdb497bc2cad9e28fca6e0f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f86a8a043423dab082ba55e203ab87d8e04053e4aa667fd0db3af829afe7296a528af8f7be895a69b8dce851d3e515538771ee50d13ad01d0ae073a775cd597
|
|
7
|
+
data.tar.gz: 48149823ec2ce21517a1c9479b4c9579390a699ece675813cf3e4cccadfb5693eb4ee5886c02c45410353afd4fa8b1d0f100ce68b536757e6f6eb99143d62b49
|
data/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Connect to your Google account. Opens a browser for OAuth authentication when no
|
|
|
54
54
|
flycal login
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
If already connected, the command reports the current status and suggests running `flycal
|
|
57
|
+
If already connected, the command reports the current status and suggests running `flycal config` to set the default calendar.
|
|
58
58
|
|
|
59
59
|
### logout
|
|
60
60
|
|
|
@@ -84,12 +84,35 @@ flycal -v
|
|
|
84
84
|
|
|
85
85
|
### calendars
|
|
86
86
|
|
|
87
|
-
List available calendars
|
|
87
|
+
List available calendars with name and ID.
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
90
|
flycal calendars
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
Example output:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Work user@example.com
|
|
97
|
+
Personal user@gmail.com
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### config
|
|
101
|
+
|
|
102
|
+
Interactive configuration menu.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
flycal config
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Options:
|
|
109
|
+
|
|
110
|
+
- `calendar_default` — choose the default calendar and save it to `~/.flycal/config.yml`
|
|
111
|
+
- `exclude_calendars` — multi-select one or more calendars whose events block free slots (default selection includes the current default calendar)
|
|
112
|
+
- `edit config` — open `~/.flycal/config.yml` with `$EDITOR` (or `vi`)
|
|
113
|
+
|
|
114
|
+
Press Ctrl+C during configuration to cancel without error (`config cancelled...`).
|
|
115
|
+
|
|
93
116
|
The default calendar is used by the search command when no calendar is specified.
|
|
94
117
|
|
|
95
118
|
### search
|
|
@@ -105,10 +128,10 @@ flycal search -i 2months -d placeholder
|
|
|
105
128
|
|
|
106
129
|
**Options:**
|
|
107
130
|
|
|
108
|
-
- `--from` / `-f` — Start date/time. Default: midnight of current day.
|
|
109
|
-
- `--to` / `-t` — End date/time. Default: 23:59 of the 30th day from today.
|
|
131
|
+
- `--from` / `-f` — Start date/time. Default: midnight of current day. Formats: `YYYY-MM-DD`, locale forms (`DD-MM-YYYY` for `it`, `MM-DD-YYYY` for `en`, `/` or `-`), or with time `YYYY-MM-DDTHH:MM`
|
|
132
|
+
- `--to` / `-t` — End date/time. Default: 23:59 of the 30th day from today. Same formats as `--from`
|
|
110
133
|
- `--in` / `-i` — Duration from `--from`, overrides `--to`. Format: `30days`, `48hours`, `2months`, `1year` (no space). With space use quotes: `--in "30 days"`
|
|
111
|
-
- `--calendar` / `-c` — Calendar name or ID. Default: calendar set via `flycal
|
|
134
|
+
- `--calendar` / `-c` — Calendar name or ID. Default: calendar set via `flycal config`
|
|
112
135
|
- `--description` / `-d` — Filter events by text. Matches events where the string appears in title or description (case-insensitive, contains)
|
|
113
136
|
|
|
114
137
|
**Time range behavior:**
|
|
@@ -130,36 +153,72 @@ For time frames longer than 7 days, a weekly breakdown is added (week number, st
|
|
|
130
153
|
|
|
131
154
|
### slots
|
|
132
155
|
|
|
133
|
-
Find free time slots in your calendar
|
|
156
|
+
Find free time slots in your calendar. Output is a simple list for copy/paste into email or other tools.
|
|
134
157
|
|
|
135
158
|
```bash
|
|
136
|
-
flycal slots
|
|
137
|
-
flycal slots --
|
|
138
|
-
flycal slots --in "
|
|
159
|
+
flycal slots
|
|
160
|
+
flycal slots --duration 1h
|
|
161
|
+
flycal slots --from 2026-08-01 --in "2 weeks" --duration 1h
|
|
162
|
+
flycal slots --from 01/08/2026 --in 3days --duration 30min
|
|
163
|
+
flycal slots --in 1week -c Work
|
|
139
164
|
```
|
|
140
165
|
|
|
141
166
|
**Options:**
|
|
142
167
|
|
|
143
|
-
- `--duration` —
|
|
144
|
-
- `--
|
|
145
|
-
- `--
|
|
168
|
+
- `--duration` — Slot length. Default: `slots.defaults.default_duration` in config (`45min`). Examples: `1h`, `30 minutes`
|
|
169
|
+
- `--from` / `-f` — Start date/time. Default: `slots.defaults.from` in config (`now`). If a date without time is today, starts from the current time.
|
|
170
|
+
- `--in` / `-i` — Search window from `--from`. Default: `1 week`.
|
|
171
|
+
- `--calendar` / `-c` — Calendar name or ID used as fallback when `exclude_calendars` is not configured
|
|
172
|
+
|
|
173
|
+
**Output:**
|
|
174
|
+
|
|
175
|
+
Always starts with a header and clickable calendar links (OSC 8), then the availability list:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
found 3 slots
|
|
179
|
+
from wed 29 July 2026 to wed 5 August 2026
|
|
180
|
+
with duration 45min
|
|
181
|
+
for calendars
|
|
182
|
+
Incode - Antonio
|
|
183
|
+
Personal
|
|
184
|
+
https://calendar.google.com/calendar/r/day/2026/7/29
|
|
185
|
+
|
|
186
|
+
friday 15/7
|
|
187
|
+
10-12
|
|
188
|
+
13-15.30
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If `pbcopy` is available (macOS), the slot list without the header is also copied to the clipboard.
|
|
146
192
|
|
|
147
193
|
Slot search windows are configured in `~/.flycal/config.yml`:
|
|
148
194
|
|
|
149
195
|
```yaml
|
|
150
196
|
slots:
|
|
151
|
-
|
|
197
|
+
defaults:
|
|
198
|
+
from: now
|
|
199
|
+
default_duration: 45min
|
|
200
|
+
free_before: 0m
|
|
201
|
+
free_after: 15m
|
|
202
|
+
exclude_calendars:
|
|
203
|
+
- user@example.com
|
|
204
|
+
- user@gmail.com
|
|
205
|
+
hours:
|
|
152
206
|
- 9:30-13:00
|
|
153
207
|
- 14:00-18:30
|
|
154
|
-
|
|
208
|
+
weekdays_only: true
|
|
155
209
|
locale: en
|
|
156
210
|
```
|
|
157
211
|
|
|
158
212
|
Missing keys are filled from `config/defaults.yml` in the gem and saved to your `config.yml` on first read.
|
|
159
213
|
|
|
160
|
-
- `
|
|
161
|
-
- `
|
|
162
|
-
- `
|
|
214
|
+
- `defaults.from` — default `--from` for slots (`now` or a date string)
|
|
215
|
+
- `defaults.default_duration` — default slot length when `--duration` is omitted
|
|
216
|
+
- `free_before` — buffer before each slot; busy intervals are extended backward so no event may fall in this window before a slot
|
|
217
|
+
- `free_after` — buffer after each slot; gaps must fit `duration + free_after`, but output shows slots of `duration` only
|
|
218
|
+
- `exclude_calendars` lists calendars whose events block free slots; if empty or not set, events from `calendar_default` are used
|
|
219
|
+
- `hours` accepts one or more ranges (`H-H`, `HH:MM-HH:MM`, mixed)
|
|
220
|
+
- `weekdays_only: true` limits slots to Monday-Friday
|
|
221
|
+
- `weekdays_only: false` includes weekends
|
|
163
222
|
- `locale` supports `en` and `it` (default is `en`)
|
|
164
223
|
|
|
165
224
|
**Output example:**
|
data/config/defaults.yml
CHANGED
data/lib/flycal_cli/cli.rb
CHANGED
|
@@ -23,7 +23,7 @@ module FlycalCli
|
|
|
23
23
|
apply_locale_override
|
|
24
24
|
if Auth.logged_in?
|
|
25
25
|
puts "✓ You are already connected to your Google account."
|
|
26
|
-
puts "\nRun 'flycal
|
|
26
|
+
puts "\nRun 'flycal config' to set the default calendar."
|
|
27
27
|
return
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -41,7 +41,7 @@ module FlycalCli
|
|
|
41
41
|
begin
|
|
42
42
|
Auth.login
|
|
43
43
|
puts "\n✓ Authentication completed successfully!"
|
|
44
|
-
puts "\nRun 'flycal
|
|
44
|
+
puts "\nRun 'flycal config' to set the default calendar."
|
|
45
45
|
rescue FlycalCli::Error => e
|
|
46
46
|
puts "Error: #{e.message}"
|
|
47
47
|
exit 1
|
|
@@ -60,49 +60,51 @@ module FlycalCli
|
|
|
60
60
|
puts "✓ Disconnected successfully."
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
desc "calendars", "List available calendars
|
|
63
|
+
desc "calendars", "List available calendars"
|
|
64
64
|
def calendars
|
|
65
65
|
apply_locale_override
|
|
66
66
|
unless Auth.logged_in?
|
|
67
|
-
puts "
|
|
67
|
+
puts Locale.t("errors.not_connected")
|
|
68
68
|
exit 1
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
spinner = TTY::Spinner.new("Loading calendars... ", format: :dots)
|
|
75
|
-
spinner.auto_spin
|
|
76
|
-
calendars = service.list_calendars
|
|
77
|
-
spinner.stop("✓")
|
|
71
|
+
calendars = load_calendars
|
|
72
|
+
return if calendars.nil?
|
|
78
73
|
|
|
79
|
-
|
|
80
|
-
puts "No calendars found."
|
|
81
|
-
return
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Build selection list (display => calendar_id)
|
|
85
|
-
default_id = Config.calendar_default
|
|
86
|
-
choices = calendars.to_h do |cal|
|
|
87
|
-
primary = cal.primary ? " (primary)" : ""
|
|
88
|
-
default = (cal.id == default_id) ? " [default]" : ""
|
|
74
|
+
calendars.each do |cal|
|
|
89
75
|
summary = cal.summary || cal.id
|
|
90
|
-
|
|
91
|
-
[label, cal.id]
|
|
76
|
+
puts "#{summary} #{cal.id}"
|
|
92
77
|
end
|
|
78
|
+
end
|
|
93
79
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
80
|
+
desc "config", "Configure flycal settings"
|
|
81
|
+
def config
|
|
82
|
+
apply_locale_override
|
|
83
|
+
unless Auth.logged_in?
|
|
84
|
+
puts Locale.t("errors.not_connected")
|
|
85
|
+
exit 1
|
|
86
|
+
end
|
|
101
87
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
88
|
+
with_config_interrupt_handling do
|
|
89
|
+
prompt = TTY::Prompt.new
|
|
90
|
+
choice = prompt.select(
|
|
91
|
+
Locale.t("config.prompt"),
|
|
92
|
+
{
|
|
93
|
+
Locale.t("config.options.calendar_default") => :calendar_default,
|
|
94
|
+
Locale.t("config.options.exclude_calendars") => :exclude_calendars,
|
|
95
|
+
Locale.t("config.options.edit_config") => :edit_config
|
|
96
|
+
},
|
|
97
|
+
per_page: 10
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
case choice
|
|
101
|
+
when :calendar_default
|
|
102
|
+
config_calendar_default
|
|
103
|
+
when :exclude_calendars
|
|
104
|
+
config_exclude_calendars
|
|
105
|
+
when :edit_config
|
|
106
|
+
config_edit
|
|
107
|
+
end
|
|
106
108
|
end
|
|
107
109
|
end
|
|
108
110
|
|
|
@@ -135,12 +137,12 @@ module FlycalCli
|
|
|
135
137
|
exit 1
|
|
136
138
|
end
|
|
137
139
|
|
|
138
|
-
time_min = options[:from].to_s.empty? ? Date.today.to_time :
|
|
140
|
+
time_min = options[:from].to_s.empty? ? Date.today.to_time : DateTimeParser.parse(options[:from])
|
|
139
141
|
begin
|
|
140
142
|
time_max = if options[:in].to_s.empty?
|
|
141
|
-
options[:to].to_s.empty? ? (Date.today + 30).to_time + 86400 - 1 :
|
|
143
|
+
options[:to].to_s.empty? ? (Date.today + 30).to_time + 86400 - 1 : DateTimeParser.parse(options[:to], end_of_day: true)
|
|
142
144
|
else
|
|
143
|
-
|
|
145
|
+
DurationParser.add_to_time(options[:in], time_min)
|
|
144
146
|
end
|
|
145
147
|
rescue FlycalCli::Error => e
|
|
146
148
|
puts "Error: #{e.message}"
|
|
@@ -176,14 +178,22 @@ module FlycalCli
|
|
|
176
178
|
long_desc <<-LONGDESC
|
|
177
179
|
List free time slots long enough for a given duration.
|
|
178
180
|
|
|
181
|
+
Defaults:
|
|
182
|
+
--from from config (default: now)
|
|
183
|
+
--in 1 week
|
|
184
|
+
--duration from config (default: 45min)
|
|
185
|
+
|
|
179
186
|
Examples:
|
|
180
|
-
flycal slots
|
|
181
|
-
flycal slots --
|
|
187
|
+
flycal slots
|
|
188
|
+
flycal slots --duration 1h
|
|
189
|
+
flycal slots --from 2026-08-01 --in "2 weeks" --duration 1h
|
|
182
190
|
LONGDESC
|
|
183
|
-
option :duration, type: :string,
|
|
184
|
-
desc: "
|
|
185
|
-
option :in, type: :string, aliases: "-i",
|
|
186
|
-
desc: "Search window from
|
|
191
|
+
option :duration, type: :string,
|
|
192
|
+
desc: "Slot length (default from config: slots.defaults.default_duration)"
|
|
193
|
+
option :in, type: :string, aliases: "-i", default: "1 week",
|
|
194
|
+
desc: "Search window from --from (default: 1 week)"
|
|
195
|
+
option :from, type: :string, aliases: "-f",
|
|
196
|
+
desc: "Start date/time (default from config: slots.defaults.from)"
|
|
187
197
|
option :calendar, type: :string, aliases: "-c", desc: "Calendar name or ID"
|
|
188
198
|
def slots
|
|
189
199
|
apply_locale_override
|
|
@@ -192,13 +202,27 @@ module FlycalCli
|
|
|
192
202
|
exit 1
|
|
193
203
|
end
|
|
194
204
|
|
|
205
|
+
slot_cfg = Config.slots_config
|
|
206
|
+
defaults = slot_cfg.fetch("defaults", {})
|
|
207
|
+
in_value = options[:in].to_s.strip
|
|
208
|
+
in_value = "1 week" if in_value.empty?
|
|
209
|
+
|
|
210
|
+
duration_value = options[:duration].to_s.strip
|
|
211
|
+
duration_value = defaults["default_duration"].to_s.strip if duration_value.empty?
|
|
212
|
+
duration_value = "45min" if duration_value.empty?
|
|
213
|
+
|
|
214
|
+
from_value = options[:from].to_s.strip
|
|
215
|
+
from_value = defaults["from"].to_s.strip if from_value.empty?
|
|
216
|
+
from_value = "now" if from_value.empty?
|
|
217
|
+
|
|
195
218
|
begin
|
|
196
|
-
slot_duration = DurationParser.to_seconds(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
219
|
+
slot_duration = DurationParser.to_seconds(duration_value)
|
|
220
|
+
free_before = DurationParser.to_seconds(slot_cfg["free_before"] || "0m")
|
|
221
|
+
free_after = DurationParser.to_seconds(slot_cfg["free_after"] || "0m")
|
|
222
|
+
time_min = parse_slots_from(from_value)
|
|
223
|
+
time_max = DurationParser.add_to_time(in_value, time_min)
|
|
224
|
+
workhours = parse_workhours(slot_cfg["hours"])
|
|
225
|
+
weekdays_only = !!slot_cfg["weekdays_only"]
|
|
202
226
|
rescue FlycalCli::Error => e
|
|
203
227
|
puts "Error: #{e.message}"
|
|
204
228
|
exit 1
|
|
@@ -212,17 +236,30 @@ module FlycalCli
|
|
|
212
236
|
creds = Auth.credentials
|
|
213
237
|
service = CalendarService.new(creds)
|
|
214
238
|
|
|
215
|
-
|
|
216
|
-
if
|
|
239
|
+
exclude_calendar_ids = resolve_slots_exclude_calendar_ids(service, slot_cfg, options[:calendar])
|
|
240
|
+
if exclude_calendar_ids.empty?
|
|
217
241
|
puts Locale.t("slots.no_calendar")
|
|
218
242
|
exit 1
|
|
219
243
|
end
|
|
220
244
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
calendars = service.list_calendars
|
|
246
|
+
calendar_meta = exclude_calendar_ids.filter_map do |id|
|
|
247
|
+
cal = calendars.find { |c| c.id == id }
|
|
248
|
+
name = cal&.summary || id
|
|
249
|
+
{ id: id, name: name }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
fetch_from = time_min - free_before
|
|
253
|
+
events = exclude_calendar_ids.flat_map do |calendar_id|
|
|
254
|
+
service.list_events(
|
|
255
|
+
calendar_id,
|
|
256
|
+
time_min: fetch_from,
|
|
257
|
+
time_max: time_max
|
|
258
|
+
)
|
|
259
|
+
rescue Google::Apis::Errors::Error => e
|
|
260
|
+
warn Locale.t("errors.calendar_fetch", calendar: calendar_id, message: e.message)
|
|
261
|
+
[]
|
|
262
|
+
end
|
|
226
263
|
|
|
227
264
|
finder = SlotFinder.new(
|
|
228
265
|
events: events,
|
|
@@ -230,11 +267,28 @@ module FlycalCli
|
|
|
230
267
|
time_max: time_max,
|
|
231
268
|
slot_duration_seconds: slot_duration,
|
|
232
269
|
workhours: workhours,
|
|
233
|
-
weekdays_only: weekdays_only
|
|
270
|
+
weekdays_only: weekdays_only,
|
|
271
|
+
free_before_seconds: free_before,
|
|
272
|
+
free_after_seconds: free_after
|
|
234
273
|
)
|
|
235
274
|
|
|
236
|
-
|
|
237
|
-
|
|
275
|
+
slots_by_day = finder.slots_by_day
|
|
276
|
+
slot_count = slots_by_day.values.sum(&:size)
|
|
277
|
+
puts SlotFormatter.format_header(
|
|
278
|
+
from: time_min,
|
|
279
|
+
to: time_max,
|
|
280
|
+
duration: duration_value,
|
|
281
|
+
calendars: calendar_meta,
|
|
282
|
+
count: slot_count
|
|
283
|
+
)
|
|
284
|
+
puts ""
|
|
285
|
+
output = SlotFormatter.format_output(slots_by_day)
|
|
286
|
+
if output.empty?
|
|
287
|
+
puts Locale.t("slots.no_available")
|
|
288
|
+
else
|
|
289
|
+
puts output
|
|
290
|
+
copy_slots_to_clipboard(output)
|
|
291
|
+
end
|
|
238
292
|
end
|
|
239
293
|
|
|
240
294
|
desc "update", "Update flycal-cli to the latest gem version"
|
|
@@ -264,44 +318,174 @@ module FlycalCli
|
|
|
264
318
|
"\e[1m#{str}\e[0m"
|
|
265
319
|
end
|
|
266
320
|
|
|
321
|
+
def bold_underline(str)
|
|
322
|
+
"\e[1;4m#{str}\e[0m"
|
|
323
|
+
end
|
|
324
|
+
|
|
267
325
|
def apply_locale_override
|
|
268
326
|
Locale.override!(options[:locale])
|
|
269
327
|
end
|
|
270
328
|
|
|
271
|
-
def
|
|
272
|
-
|
|
329
|
+
def with_config_interrupt_handling
|
|
330
|
+
yield
|
|
331
|
+
rescue Interrupt
|
|
332
|
+
puts "\nconfig cancelled..."
|
|
333
|
+
exit 0
|
|
334
|
+
end
|
|
273
335
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
336
|
+
def load_calendars
|
|
337
|
+
creds = Auth.credentials
|
|
338
|
+
service = CalendarService.new(creds)
|
|
339
|
+
|
|
340
|
+
spinner = TTY::Spinner.new("#{Locale.t('common.loading_calendars')} ", format: :dots)
|
|
341
|
+
spinner.auto_spin
|
|
342
|
+
calendars = service.list_calendars
|
|
343
|
+
spinner.stop("✓")
|
|
344
|
+
|
|
345
|
+
if calendars.empty?
|
|
346
|
+
puts Locale.t("errors.no_calendars")
|
|
347
|
+
return nil
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
calendars
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def calendar_choices(calendars, highlight_ids: [])
|
|
354
|
+
highlights = Array(highlight_ids).compact
|
|
355
|
+
calendars.to_h do |cal|
|
|
356
|
+
primary = cal.primary ? " (primary)" : ""
|
|
357
|
+
summary = cal.summary || cal.id
|
|
358
|
+
label = "#{summary}#{primary}"
|
|
359
|
+
label = bold_underline(label) if highlights.include?(cal.id)
|
|
360
|
+
[label, cal.id]
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def choice_labels_for_ids(choices, calendar_ids)
|
|
365
|
+
ids = Array(calendar_ids).compact
|
|
366
|
+
choices.select { |_label, id| ids.include?(id) }.keys
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def config_calendar_default
|
|
370
|
+
calendars = load_calendars
|
|
371
|
+
return if calendars.nil?
|
|
372
|
+
|
|
373
|
+
prompt = TTY::Prompt.new
|
|
374
|
+
default_id = Config.calendar_default
|
|
375
|
+
selected_id = prompt.select(
|
|
376
|
+
Locale.t("config.calendar_default.prompt"),
|
|
377
|
+
calendar_choices(calendars, highlight_ids: [default_id]),
|
|
378
|
+
per_page: 15,
|
|
379
|
+
filter: true
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
calendar = calendars.find { |c| c.id == selected_id }
|
|
383
|
+
return unless calendar
|
|
384
|
+
|
|
385
|
+
Config.calendar_default = calendar.id
|
|
386
|
+
puts Locale.t("config.calendar_default.saved", name: calendar.summary || calendar.id)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def config_exclude_calendars
|
|
390
|
+
calendars = load_calendars
|
|
391
|
+
return if calendars.nil?
|
|
392
|
+
|
|
393
|
+
prompt = TTY::Prompt.new
|
|
394
|
+
default_id = Config.calendar_default
|
|
395
|
+
configured = Config.exclude_calendars
|
|
396
|
+
current_ids = resolve_calendar_refs(calendars, configured)
|
|
397
|
+
preselected_ids = current_ids.empty? && default_id ? [default_id] : current_ids
|
|
398
|
+
choices = calendar_choices(calendars, highlight_ids: current_ids)
|
|
399
|
+
preselected_labels = choice_labels_for_ids(choices, preselected_ids)
|
|
400
|
+
|
|
401
|
+
selected_ids = prompt.multi_select(
|
|
402
|
+
Locale.t("config.exclude_calendars.prompt"),
|
|
403
|
+
choices,
|
|
404
|
+
default: preselected_labels,
|
|
405
|
+
min: 1,
|
|
406
|
+
per_page: 15,
|
|
407
|
+
filter: true
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
Config.exclude_calendars = selected_ids
|
|
411
|
+
names = selected_ids.map do |id|
|
|
412
|
+
calendars.find { |c| c.id == id }&.summary || id
|
|
280
413
|
end
|
|
414
|
+
puts Locale.t("config.exclude_calendars.saved", names: names.join(", "))
|
|
281
415
|
end
|
|
282
416
|
|
|
283
|
-
def
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
raise FlycalCli::Error, "Invalid --in format. Use: 30days, 48hours, 2months, 1year (no space, or quote: --in \"30 days\")" unless m
|
|
417
|
+
def config_edit
|
|
418
|
+
editor = ENV["EDITOR"].to_s.strip
|
|
419
|
+
editor = "vi" if editor.empty?
|
|
287
420
|
|
|
288
|
-
|
|
289
|
-
|
|
421
|
+
success = system(editor, Config.config_file)
|
|
422
|
+
puts Locale.t("config.edit.failed", editor: editor) unless success
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def resolve_calendar_refs(calendars, refs)
|
|
426
|
+
Array(refs).filter_map do |ref|
|
|
427
|
+
resolve_calendar_ref(calendars, ref)
|
|
428
|
+
end.uniq
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def resolve_calendar_ref(calendars, ref)
|
|
432
|
+
ref = ref.to_s.strip
|
|
433
|
+
return nil if ref.empty?
|
|
290
434
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
435
|
+
exact = calendars.find { |cal| cal.id == ref }
|
|
436
|
+
return exact.id if exact
|
|
437
|
+
|
|
438
|
+
matches = calendars.select do |cal|
|
|
439
|
+
cal.id == ref ||
|
|
440
|
+
(cal.summary && cal.summary.downcase.include?(ref.downcase))
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
matches.first&.id
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def resolve_slots_exclude_calendar_ids(service, slot_cfg, calendar_override)
|
|
447
|
+
configured = slot_cfg["exclude_calendars"]
|
|
448
|
+
calendars = service.list_calendars
|
|
449
|
+
|
|
450
|
+
refs = if configured.nil? || (configured.is_a?(Array) && configured.empty?)
|
|
451
|
+
fallback = calendar_override || Config.calendar_default
|
|
452
|
+
fallback ? [fallback] : []
|
|
453
|
+
else
|
|
454
|
+
configured
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
ids = resolve_calendar_refs(calendars, refs)
|
|
458
|
+
return ids unless ids.empty?
|
|
459
|
+
|
|
460
|
+
fallback = calendar_override || Config.calendar_default
|
|
461
|
+
if fallback
|
|
462
|
+
resolve_calendar_refs(calendars, [fallback])
|
|
300
463
|
else
|
|
301
|
-
|
|
464
|
+
primary = calendars.find(&:primary)
|
|
465
|
+
primary ? [primary.id] : calendars.first(1).map(&:id)
|
|
302
466
|
end
|
|
303
467
|
end
|
|
304
468
|
|
|
469
|
+
def parse_slots_from(value)
|
|
470
|
+
return Time.now if value.to_s.strip.downcase == "now"
|
|
471
|
+
|
|
472
|
+
DateTimeParser.parse(value)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def copy_slots_to_clipboard(text)
|
|
476
|
+
return unless pbcopy_available?
|
|
477
|
+
|
|
478
|
+
IO.popen("pbcopy", "w") { |io| io.write(text) }
|
|
479
|
+
puts ""
|
|
480
|
+
puts Locale.t("slots.copied_to_clipboard")
|
|
481
|
+
rescue StandardError
|
|
482
|
+
nil
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def pbcopy_available?
|
|
486
|
+
system("which", "pbcopy", out: File::NULL, err: File::NULL)
|
|
487
|
+
end
|
|
488
|
+
|
|
305
489
|
def parse_hour_minute(value)
|
|
306
490
|
match = value.to_s.strip.match(/\A(\d{1,2}):(\d{2})\z/)
|
|
307
491
|
raise FlycalCli::Error, "Invalid time format #{value.inspect}. Use HH:MM (e.g. 9:00, 18:30)." unless match
|
|
@@ -318,18 +502,18 @@ module FlycalCli
|
|
|
318
502
|
def parse_workhours(values)
|
|
319
503
|
ranges = Array(values).map do |item|
|
|
320
504
|
start_str, end_str = item.to_s.strip.split("-", 2)
|
|
321
|
-
raise FlycalCli::Error, "Invalid
|
|
505
|
+
raise FlycalCli::Error, "Invalid hours item #{item.inspect}. Use format like '9-13' or '14:30-18:00'." if start_str.nil? || end_str.nil?
|
|
322
506
|
|
|
323
507
|
sh, sm = parse_hour_minute_flexible(start_str)
|
|
324
508
|
eh, em = parse_hour_minute_flexible(end_str)
|
|
325
509
|
start_minutes = (sh * 60) + sm
|
|
326
510
|
end_minutes = (eh * 60) + em
|
|
327
|
-
raise FlycalCli::Error, "Invalid
|
|
511
|
+
raise FlycalCli::Error, "Invalid hours range #{item.inspect}: end must be after start." if end_minutes <= start_minutes
|
|
328
512
|
|
|
329
513
|
[sh, sm, eh, em]
|
|
330
514
|
end
|
|
331
515
|
|
|
332
|
-
raise FlycalCli::Error, "slots.
|
|
516
|
+
raise FlycalCli::Error, "slots.hours cannot be empty in config.yml." if ranges.empty?
|
|
333
517
|
|
|
334
518
|
ranges.sort_by { |sh, sm, _eh, _em| (sh * 60) + sm }
|
|
335
519
|
end
|
|
@@ -371,22 +555,6 @@ module FlycalCli
|
|
|
371
555
|
matches.map(&:id)
|
|
372
556
|
end
|
|
373
557
|
|
|
374
|
-
def resolve_single_calendar_id(service, calendar_name)
|
|
375
|
-
if calendar_name && !calendar_name.empty?
|
|
376
|
-
ids = resolve_calendar_ids(service, calendar_name)
|
|
377
|
-
return ids.first
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
default_id = Config.calendar_default
|
|
381
|
-
return default_id if default_id
|
|
382
|
-
|
|
383
|
-
calendars = service.list_calendars
|
|
384
|
-
primary = calendars.find(&:primary)
|
|
385
|
-
return primary.id if primary
|
|
386
|
-
|
|
387
|
-
calendars.first&.id
|
|
388
|
-
end
|
|
389
|
-
|
|
390
558
|
def print_events(service, events)
|
|
391
559
|
# Use calendar list for names (avoids extra API calls)
|
|
392
560
|
calendar_list = service.list_calendars
|