flycal-cli 0.4.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61b9a15ce2341c56bbac3c59bd465e02a13082e817a367cc19467dc88e874b76
4
- data.tar.gz: d6cf5b95459a56289a5e9d8b22c706be82f85cc51ddcb42dbcb30b4c7b0b1e66
3
+ metadata.gz: 980bab82020402b0031c0fc3d84a240626a8a26145cc8a86973390777b21db12
4
+ data.tar.gz: b26c8bc3fec6e7f68a291d4e6124fc41680c15e5204a1217d6331d5c097cdf91
5
5
  SHA512:
6
- metadata.gz: 58585691471f02638b1490300ce22a722bad34bcb3e53f2069975697385d8e4cb313b7311fd68dbbdd1fbfc38bcd77ee1f95a600f5c2b56394c7317e70b9b029
7
- data.tar.gz: 6b2b9fd98a5e629b27e4117ba135c72de9e348cfbba04b494b68b3291554b29b3a5c7233d32a330de4879da213870a17bb537d7e3f23fd39a81219607a8b501c
6
+ metadata.gz: bce52224114636c4a330ebcdd6740409add46791789544e42cd04380d080056d5b9dddabe8ad0a4a395448958c23631702c05864b3617d3e4ee9fe9eda616b3b
7
+ data.tar.gz: fa998fbf3213fabd004a58e4d709fd05f28eee1951b9efef4c7c9ccc17fcf903af73d8e9cf7e0345569b774c8369f6422cdf4f018cd7ca1dc92e5126f0730aa8
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 calendars` to set the default calendar.
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 and set the default one. Uses an interactive scrollable menu (arrow keys to navigate, type to filter).
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
@@ -108,7 +131,7 @@ flycal search -i 2months -d placeholder
108
131
  - `--from` / `-f` — Start date/time. Default: midnight of current day. Format: `2025-01-01` or `2025-01-01T09:00`
109
132
  - `--to` / `-t` — End date/time. Default: 23:59 of the 30th day from today. Format: same 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 calendars`
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:**
@@ -142,24 +165,28 @@ flycal slots --in "48 hours" --duration 1hour -c Work
142
165
 
143
166
  - `--duration` — Minimum slot length. Examples: `1h`, `1 hour`, `30 minutes`, `90min`
144
167
  - `--in` / `-i` — Search window from now. Examples: `3 days`, `1 week`, `48 hours` (use quotes when the value contains a space)
145
- - `--calendar` / `-c` — Calendar name or ID (optional; uses default calendar)
168
+ - `--calendar` / `-c` — Calendar name or ID used as fallback when `exclude_calendars` is not configured
146
169
 
147
170
  Slot search windows are configured in `~/.flycal/config.yml`:
148
171
 
149
172
  ```yaml
150
173
  slots:
174
+ exclude_calendars:
175
+ - user@example.com
176
+ - user@gmail.com
151
177
  workhours:
152
178
  - 9:30-13:00
153
179
  - 14:00-18:30
154
- weekdays-only: true
180
+ weekdays_only: true
155
181
  locale: en
156
182
  ```
157
183
 
158
184
  Missing keys are filled from `config/defaults.yml` in the gem and saved to your `config.yml` on first read.
159
185
 
186
+ - `exclude_calendars` lists calendars whose events block free slots; if empty or not set, events from `calendar_default` are used
160
187
  - `workhours` accepts one or more ranges (`H-H`, `HH:MM-HH:MM`, mixed)
161
- - `weekdays-only: true` limits slots to Monday-Friday
162
- - `weekdays-only: false` includes weekends
188
+ - `weekdays_only: true` limits slots to Monday-Friday
189
+ - `weekdays_only: false` includes weekends
163
190
  - `locale` supports `en` and `it` (default is `en`)
164
191
 
165
192
  **Output example:**
data/config/defaults.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  locale: en
2
2
  slots:
3
+ exclude_calendars: []
3
4
  workhours:
4
5
  - 9:30-13:00
5
6
  - 14:00-18:30
6
- weekdays-only: true
7
+ weekdays_only: true
@@ -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 calendars' to set the default calendar."
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 calendars' to set the default calendar."
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 and set the default one"
63
+ desc "calendars", "List available calendars"
64
64
  def calendars
65
65
  apply_locale_override
66
66
  unless Auth.logged_in?
67
- puts "You are not connected. Run 'flycal login' first."
67
+ puts Locale.t("errors.not_connected")
68
68
  exit 1
69
69
  end
70
70
 
71
- creds = Auth.credentials
72
- service = CalendarService.new(creds)
73
-
74
- spinner = TTY::Spinner.new("Loading calendars... ", format: :dots)
75
- spinner.auto_spin
76
- calendars = service.list_calendars
77
- spinner.stop("✓")
78
-
79
- if calendars.empty?
80
- puts "No calendars found."
81
- return
82
- end
71
+ calendars = load_calendars
72
+ return if calendars.nil?
83
73
 
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
- label = "#{summary}#{primary}#{default}"
91
- [label, cal.id]
76
+ puts "#{summary} #{cal.id}"
92
77
  end
78
+ end
93
79
 
94
- prompt = TTY::Prompt.new
95
- selected_id = prompt.select(
96
- "Choose the default calendar:",
97
- choices,
98
- per_page: 15,
99
- filter: true
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
- calendar = calendars.find { |c| c.id == selected_id }
103
- if calendar
104
- Config.calendar_default = calendar.id
105
- puts "\n✓ Default calendar set to: #{calendar.summary}"
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
 
@@ -198,7 +200,7 @@ module FlycalCli
198
200
  time_max = DurationParser.add_to_time(options[:in], time_min)
199
201
  slot_cfg = Config.slots_config
200
202
  workhours = parse_workhours(slot_cfg["workhours"])
201
- weekdays_only = !!slot_cfg["weekdays-only"]
203
+ weekdays_only = !!slot_cfg["weekdays_only"]
202
204
  rescue FlycalCli::Error => e
203
205
  puts "Error: #{e.message}"
204
206
  exit 1
@@ -212,17 +214,22 @@ module FlycalCli
212
214
  creds = Auth.credentials
213
215
  service = CalendarService.new(creds)
214
216
 
215
- calendar_id = resolve_single_calendar_id(service, options[:calendar])
216
- if calendar_id.nil?
217
+ exclude_calendar_ids = resolve_slots_exclude_calendar_ids(service, slot_cfg, options[:calendar])
218
+ if exclude_calendar_ids.empty?
217
219
  puts Locale.t("slots.no_calendar")
218
220
  exit 1
219
221
  end
220
222
 
221
- events = service.list_events(
222
- calendar_id,
223
- time_min: time_min,
224
- time_max: time_max
225
- )
223
+ events = exclude_calendar_ids.flat_map do |calendar_id|
224
+ service.list_events(
225
+ calendar_id,
226
+ time_min: time_min,
227
+ time_max: time_max
228
+ )
229
+ rescue Google::Apis::Errors::Error => e
230
+ warn Locale.t("errors.calendar_fetch", calendar: calendar_id, message: e.message)
231
+ []
232
+ end
226
233
 
227
234
  finder = SlotFinder.new(
228
235
  events: events,
@@ -264,10 +271,154 @@ module FlycalCli
264
271
  "\e[1m#{str}\e[0m"
265
272
  end
266
273
 
274
+ def bold_underline(str)
275
+ "\e[1;4m#{str}\e[0m"
276
+ end
277
+
267
278
  def apply_locale_override
268
279
  Locale.override!(options[:locale])
269
280
  end
270
281
 
282
+ def with_config_interrupt_handling
283
+ yield
284
+ rescue Interrupt
285
+ puts "\nconfig cancelled..."
286
+ exit 0
287
+ end
288
+
289
+ def load_calendars
290
+ creds = Auth.credentials
291
+ service = CalendarService.new(creds)
292
+
293
+ spinner = TTY::Spinner.new("#{Locale.t('common.loading_calendars')} ", format: :dots)
294
+ spinner.auto_spin
295
+ calendars = service.list_calendars
296
+ spinner.stop("✓")
297
+
298
+ if calendars.empty?
299
+ puts Locale.t("errors.no_calendars")
300
+ return nil
301
+ end
302
+
303
+ calendars
304
+ end
305
+
306
+ def calendar_choices(calendars, highlight_ids: [])
307
+ highlights = Array(highlight_ids).compact
308
+ calendars.to_h do |cal|
309
+ primary = cal.primary ? " (primary)" : ""
310
+ summary = cal.summary || cal.id
311
+ label = "#{summary}#{primary}"
312
+ label = bold_underline(label) if highlights.include?(cal.id)
313
+ [label, cal.id]
314
+ end
315
+ end
316
+
317
+ def choice_labels_for_ids(choices, calendar_ids)
318
+ ids = Array(calendar_ids).compact
319
+ choices.select { |_label, id| ids.include?(id) }.keys
320
+ end
321
+
322
+ def config_calendar_default
323
+ calendars = load_calendars
324
+ return if calendars.nil?
325
+
326
+ prompt = TTY::Prompt.new
327
+ default_id = Config.calendar_default
328
+ selected_id = prompt.select(
329
+ Locale.t("config.calendar_default.prompt"),
330
+ calendar_choices(calendars, highlight_ids: [default_id]),
331
+ per_page: 15,
332
+ filter: true
333
+ )
334
+
335
+ calendar = calendars.find { |c| c.id == selected_id }
336
+ return unless calendar
337
+
338
+ Config.calendar_default = calendar.id
339
+ puts Locale.t("config.calendar_default.saved", name: calendar.summary || calendar.id)
340
+ end
341
+
342
+ def config_exclude_calendars
343
+ calendars = load_calendars
344
+ return if calendars.nil?
345
+
346
+ prompt = TTY::Prompt.new
347
+ default_id = Config.calendar_default
348
+ configured = Config.exclude_calendars
349
+ current_ids = resolve_calendar_refs(calendars, configured)
350
+ preselected_ids = current_ids.empty? && default_id ? [default_id] : current_ids
351
+ choices = calendar_choices(calendars, highlight_ids: current_ids)
352
+ preselected_labels = choice_labels_for_ids(choices, preselected_ids)
353
+
354
+ selected_ids = prompt.multi_select(
355
+ Locale.t("config.exclude_calendars.prompt"),
356
+ choices,
357
+ default: preselected_labels,
358
+ min: 1,
359
+ per_page: 15,
360
+ filter: true
361
+ )
362
+
363
+ Config.exclude_calendars = selected_ids
364
+ names = selected_ids.map do |id|
365
+ calendars.find { |c| c.id == id }&.summary || id
366
+ end
367
+ puts Locale.t("config.exclude_calendars.saved", names: names.join(", "))
368
+ end
369
+
370
+ def config_edit
371
+ editor = ENV["EDITOR"].to_s.strip
372
+ editor = "vi" if editor.empty?
373
+
374
+ success = system(editor, Config.config_file)
375
+ puts Locale.t("config.edit.failed", editor: editor) unless success
376
+ end
377
+
378
+ def resolve_calendar_refs(calendars, refs)
379
+ Array(refs).filter_map do |ref|
380
+ resolve_calendar_ref(calendars, ref)
381
+ end.uniq
382
+ end
383
+
384
+ def resolve_calendar_ref(calendars, ref)
385
+ ref = ref.to_s.strip
386
+ return nil if ref.empty?
387
+
388
+ exact = calendars.find { |cal| cal.id == ref }
389
+ return exact.id if exact
390
+
391
+ matches = calendars.select do |cal|
392
+ cal.id == ref ||
393
+ (cal.summary && cal.summary.downcase.include?(ref.downcase))
394
+ end
395
+
396
+ matches.first&.id
397
+ end
398
+
399
+ def resolve_slots_exclude_calendar_ids(service, slot_cfg, calendar_override)
400
+ configured = slot_cfg["exclude_calendars"]
401
+ calendars = service.list_calendars
402
+
403
+ refs = if configured.nil? || (configured.is_a?(Array) && configured.empty?)
404
+ fallback = calendar_override || Config.calendar_default
405
+ fallback ? [fallback] : []
406
+ else
407
+ configured
408
+ end
409
+
410
+ ids = resolve_calendar_refs(calendars, refs)
411
+ return ids unless ids.empty?
412
+
413
+ fallback = calendar_override || Config.calendar_default
414
+ if fallback
415
+ resolve_calendar_refs(calendars, [fallback])
416
+ else
417
+ primary = calendars.find(&:primary)
418
+ primary ? [primary.id] : calendars.first(1).map(&:id)
419
+ end
420
+ end
421
+
271
422
  def parse_datetime(str, end_of_day: false)
272
423
  return nil if str.nil? || str.empty?
273
424
 
@@ -371,22 +522,6 @@ module FlycalCli
371
522
  matches.map(&:id)
372
523
  end
373
524
 
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
525
  def print_events(service, events)
391
526
  # Use calendar list for names (avoids extra API calls)
392
527
  calendar_list = service.list_calendars
@@ -20,7 +20,8 @@ module FlycalCli
20
20
  end
21
21
 
22
22
  merged, changed = merge_missing_defaults(user_data, default_values)
23
- save(merged) if changed || !File.exist?(CONFIG_FILE)
23
+ merged, migrated = normalize_slots_keys(merged)
24
+ save(merged) if changed || migrated || !File.exist?(CONFIG_FILE)
24
25
 
25
26
  merged
26
27
  end
@@ -64,6 +65,45 @@ module FlycalCli
64
65
  load.fetch("slots", {})
65
66
  end
66
67
 
68
+ def exclude_calendars
69
+ Array(slots_config["exclude_calendars"]).map(&:to_s).reject(&:empty?)
70
+ end
71
+
72
+ def exclude_calendars=(calendar_ids)
73
+ data = load
74
+ data["slots"] ||= {}
75
+ data["slots"]["exclude_calendars"] = Array(calendar_ids).map(&:to_s)
76
+ save(data)
77
+ end
78
+
79
+ def normalize_slots_keys(data)
80
+ slots = data["slots"]
81
+ return [data, false] unless slots.is_a?(Hash)
82
+
83
+ changed = false
84
+ normalized = slots.dup
85
+
86
+ if normalized.key?("exclude-calendars") && !normalized.key?("exclude_calendars")
87
+ normalized["exclude_calendars"] = normalized.delete("exclude-calendars")
88
+ changed = true
89
+ end
90
+
91
+ if normalized.key?("weekdays-only") && !normalized.key?("weekdays_only")
92
+ normalized["weekdays_only"] = normalized.delete("weekdays-only")
93
+ changed = true
94
+ end
95
+
96
+ return [data, false] unless changed
97
+
98
+ data = data.dup
99
+ data["slots"] = normalized
100
+ [data, true]
101
+ end
102
+
103
+ def config_file
104
+ CONFIG_FILE
105
+ end
106
+
67
107
  def locale
68
108
  load["locale"].to_s
69
109
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlycalCli
4
- VERSION = '0.4.1'
4
+ VERSION = '0.5.0'
5
5
  end
data/locales/en.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "common": {
3
+ "loading_calendars": "Loading calendars...",
3
4
  "weekdays": {
4
5
  "full": ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
5
6
  "abbr": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
@@ -8,12 +9,33 @@
8
9
  "full": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
9
10
  }
10
11
  },
12
+ "config": {
13
+ "prompt": "Choose a configuration option:",
14
+ "options": {
15
+ "calendar_default": "calendar_default",
16
+ "exclude_calendars": "exclude_calendars",
17
+ "edit_config": "edit config"
18
+ },
19
+ "calendar_default": {
20
+ "prompt": "Choose the default calendar:",
21
+ "saved": "✓ Default calendar set to: %{name}"
22
+ },
23
+ "exclude_calendars": {
24
+ "prompt": "Choose calendars whose events block free slots:",
25
+ "saved": "✓ Exclude calendars set to: %{names}"
26
+ },
27
+ "edit": {
28
+ "failed": "Error: could not open config with %{editor}."
29
+ }
30
+ },
11
31
  "slots": {
12
32
  "no_available": "No available slots found.",
13
- "no_calendar": "No calendar found. Run 'flycal calendars' to set a default."
33
+ "no_calendar": "No calendar found. Run 'flycal config' to set a default."
14
34
  },
15
35
  "errors": {
16
36
  "not_connected": "You are not connected. Run 'flycal login' first.",
17
- "duration_positive": "Error: --in must be a positive duration."
37
+ "duration_positive": "Error: --in must be a positive duration.",
38
+ "no_calendars": "No calendars found.",
39
+ "calendar_fetch": "Error in calendar %{calendar}: %{message}"
18
40
  }
19
41
  }
data/locales/it.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "common": {
3
+ "loading_calendars": "Caricamento calendari...",
3
4
  "weekdays": {
4
5
  "full": ["domenica", "lunedi", "martedi", "mercoledi", "giovedi", "venerdi", "sabato"],
5
6
  "abbr": ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab"]
@@ -8,12 +9,33 @@
8
9
  "full": ["gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"]
9
10
  }
10
11
  },
12
+ "config": {
13
+ "prompt": "Scegli un'opzione di configurazione:",
14
+ "options": {
15
+ "calendar_default": "calendar_default",
16
+ "exclude_calendars": "exclude_calendars",
17
+ "edit_config": "edit config"
18
+ },
19
+ "calendar_default": {
20
+ "prompt": "Scegli il calendario predefinito:",
21
+ "saved": "✓ Calendario predefinito impostato su: %{name}"
22
+ },
23
+ "exclude_calendars": {
24
+ "prompt": "Scegli i calendari i cui eventi bloccano gli slot liberi:",
25
+ "saved": "✓ Calendari esclusi impostati su: %{names}"
26
+ },
27
+ "edit": {
28
+ "failed": "Errore: impossibile aprire il config con %{editor}."
29
+ }
30
+ },
11
31
  "slots": {
12
32
  "no_available": "Nessuno slot disponibile.",
13
- "no_calendar": "Nessun calendario trovato. Esegui 'flycal calendars' per impostare il predefinito."
33
+ "no_calendar": "Nessun calendario trovato. Esegui 'flycal config' per impostare il predefinito."
14
34
  },
15
35
  "errors": {
16
36
  "not_connected": "Non sei connesso. Esegui prima 'flycal login'.",
17
- "duration_positive": "Errore: --in deve essere una durata positiva."
37
+ "duration_positive": "Errore: --in deve essere una durata positiva.",
38
+ "no_calendars": "Nessun calendario trovato.",
39
+ "calendar_fetch": "Errore nel calendario %{calendar}: %{message}"
18
40
  }
19
41
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flycal-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - flycal-cli