Class: Flycal::Cli::App

Inherits:
Thor
  • Object
show all
Defined in:
lib/flycal/cli/app.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.exit_on_failure?Boolean

Returns:

  • (Boolean)


21
22
23
# File 'lib/flycal/cli/app.rb', line 21

def self.exit_on_failure?
  true
end

Instance Method Details

#calendarsObject



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/flycal/cli/app.rb', line 62

def calendars
  apply_locale_override
  unless Auth.logged_in?
    puts Locale.t("errors.not_connected")
    exit 1
  end

  calendars = load_calendars
  return if calendars.nil?

  calendars.each do |cal|
    summary = cal.summary || cal.id
    puts "#{summary} #{cal.id}"
  end
end

#configObject



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/flycal/cli/app.rb', line 79

def config
  apply_locale_override
  unless Auth.logged_in?
    puts Locale.t("errors.not_connected")
    exit 1
  end

  with_config_interrupt_handling do
    prompt = TTY::Prompt.new
    choice = prompt.select(
      Locale.t("config.prompt"),
      {
        Locale.t("config.options.calendar_default") => :calendar_default,
        Locale.t("config.options.exclude_calendars") => :exclude_calendars,
        Locale.t("config.options.edit_config") => :edit_config
      },
      per_page: 10
    )

    case choice
    when :calendar_default
      config_calendar_default
    when :exclude_calendars
      config_exclude_calendars
    when :edit_config
      config_edit
    end
  end
end

#loginObject



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/flycal/cli/app.rb', line 28

def 
  apply_locale_override
  if Auth.logged_in?
    puts Locale.t("login.already_connected")
    Hooks.
    return
  end

  mode = 
  return unless mode

  begin
    Auth.(mode)
    puts "\n#{Locale.t('login.success')}"
    Hooks.
  rescue Flycal::Error => e
    puts "Error: #{e.message}"
    exit 1
  end
end

#logoutObject



50
51
52
53
54
55
56
57
58
59
# File 'lib/flycal/cli/app.rb', line 50

def logout
  apply_locale_override
  unless Auth.logged_in?
    puts "You are not connected to any Google account."
    return
  end

  Auth.logout
  puts "✓ Disconnected successfully."
end

#searchObject



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/flycal/cli/app.rb', line 163

def search
  apply_locale_override

  mock_mode = Mock::Config.enabled?(options)
  mock_config = nil
  if mock_mode
    begin
      mock_config = Mock::Config.from_options(options)
    rescue Flycal::Error => e
      puts "Error: #{e.message}"
      exit 1
    end
  elsif !Auth.logged_in?
    puts "You are not connected. Run 'flycal login' first."
    exit 1
  end

  time_min = options[:from].to_s.empty? ? Date.today.to_time : DateTimeParser.parse(options[:from])
  begin
    time_max = if options[:in].to_s.empty?
                 options[:to].to_s.empty? ? (Date.today + 30).to_time + 86400 - 1 : DateTimeParser.parse(options[:to], end_of_day: true)
               else
                 DurationParser.add_to_time(options[:in], time_min)
               end
  rescue Flycal::Error => e
    puts "Error: #{e.message}"
    exit 1
  end

  if time_min > time_max
    puts "Error: 'from' date must be before 'to' date."
    exit 1
  end

  service =
    if mock_config
      Mock::CalendarService.new(mock_config)
    else
      flycal_client.calendar
    end

  calendar_ids =
    if mock_config && options[:calendar].to_s.empty?
      [mock_config.calendar_name]
    else
      resolve_calendar_ids(service, options[:calendar])
    end
  if calendar_ids.empty?
    puts "No calendars found."
    exit 1
  end

  params = Pipeline::Params.new(
    command: "search",
    time_min: time_min,
    time_max: time_max,
    calendar_ids: calendar_ids,
    description: options[:description],
    calendar: options[:calendar],
    from_option: options[:from],
    to_option: options[:to],
    in_option: options[:in],
    group_by_option: options[:groupBy],
    format: options[:format] || "text",
    locale: Locale.current_locale,
    use_mock: !mock_config.nil?,
    mock_seed: mock_config&.seed,
    mock_calendar: mock_config&.calendar_name,
    mock_template: mock_config&.template_name
  )

  fingerprint = {
    command: "search",
    time_min: time_min,
    time_max: time_max,
    calendar_ids: calendar_ids,
    description: options[:description],
    calendar: options[:calendar],
    from_option: options[:from],
    to_option: options[:to],
    in_option: options[:in],
    group_by_option: options[:groupBy],
    format: options[:format] || "text",
    locale: Locale.current_locale,
    use_mock: !mock_config.nil?,
    mock_seed: mock_config&.seed,
    mock_calendar: mock_config&.calendar_name,
    mock_template: mock_config&.template_name
  }
  cache_key = Flycal::CacheKey.generate("search", fingerprint)
  ttl = Flycal::CacheKey::DEFAULT_TTL_MINUTES
  nocache = options[:nocache]

  begin
    output = nil
    status = nil

    unless nocache
      cached = FileCache.read(cache_key, ttl_minutes: ttl)
      if cached
        output = cached
        status = "HIT"
      end
    end

    if output.nil?
      FileCache.delete(cache_key) if nocache
      output = Pipeline::SearchPipeline.new(service).run(params)
      FileCache.write(cache_key, strip_cache_marker_from_output(output, params[:format]))
      status = "MISS"
    end

    print annotate_cli_output(output, status, cache_key, params[:format])
  rescue Flycal::Error => e
    puts "Error: #{e.message}"
    exit 1
  end
end

#slotsObject



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/flycal/cli/app.rb', line 311

def slots
  apply_locale_override
  unless Auth.logged_in?
    puts Locale.t("errors.not_connected")
    exit 1
  end

  slot_cfg = Config.slots_config
  defaults = slot_cfg.fetch("defaults", {})
  in_value = options[:in].to_s.strip
  in_value = "1 week" if in_value.empty?

  duration_value = options[:duration].to_s.strip
  duration_value = defaults["default_duration"].to_s.strip if duration_value.empty?
  duration_value = "45min" if duration_value.empty?

  from_value = options[:from].to_s.strip
  from_value = defaults["from"].to_s.strip if from_value.empty?
  from_value = "now" if from_value.empty?
  timezone = Config.timezone
  template_name = nil
  hours = nil
  days = nil
  slot_duration = nil
  free_before = nil
  free_after = nil
  time_min = nil
  time_max = nil

  begin
    TimeFormat.with_zone(timezone) do
      resolved = SlotTemplates.resolve(name: options[:template], catalog: slot_cfg["templates"])
      template_name = resolved[:name]
      hours = resolved[:hours]
      days = resolved[:days]
      slot_duration = DurationParser.to_seconds(duration_value)
      free_before = DurationParser.to_seconds(slot_cfg["free_before"] || "0m")
      free_after = DurationParser.to_seconds(slot_cfg["free_after"] || "0m")
      time_min = parse_slots_from(from_value)
      time_max = DurationParser.add_to_time(in_value, time_min)
    end
  rescue Flycal::Error => e
    puts "Error: #{e.message}"
    exit 1
  end

  if time_min >= time_max
    puts Locale.t("errors.duration_positive")
    exit 1
  end

  client = flycal_client
  service = client.calendar

  exclude_calendar_ids = resolve_slots_exclude_calendar_ids(service, slot_cfg, options[:calendar])
  if exclude_calendar_ids.empty?
    puts Locale.t("slots.no_calendar")
    exit 1
  end

  format = (options[:format] || "text").to_s.strip.downcase
  format = "text" if format.empty?

  unless %w[json text].include?(format)
    puts "Error: Unsupported format #{format.inspect}. Available: text, json"
    exit 1
  end

  fingerprint = {
    command: "slots",
    calendar_ids: exclude_calendar_ids,
    duration: duration_value,
    from: from_value,
    in: in_value,
    free_before: slot_cfg["free_before"] || "0m",
    free_after: slot_cfg["free_after"] || "0m",
    template: template_name,
    locale: Locale.current_locale,
    timezone: timezone,
    calendar: options[:calendar],
    hours: hours,
    days: days,
    time_min: time_min,
    time_max: time_max,
    format: format
  }
  cache_key = Flycal::CacheKey.generate("slots", fingerprint)
  ttl = Flycal::CacheKey::DEFAULT_TTL_MINUTES
  nocache = options[:nocache]

  output = nil
  status = nil
  unless nocache
    cached = FileCache.read(cache_key, ttl_minutes: ttl)
    if cached
      output = cached
      status = "HIT"
    end
  end

  if output.nil?
    FileCache.delete(cache_key) if nocache

    TimeFormat.with_zone(timezone) do
      calendars = client.list_calendars
      calendar_meta = exclude_calendar_ids.filter_map do |id|
        cal = calendars.find { |c| c.id == id }
        name = cal&.summary || id
        { id: id, name: name }
      end

      fetch_from = time_min - free_before
      events = exclude_calendar_ids.flat_map do |calendar_id|
        client.list_events(
          calendar_id,
          time_min: fetch_from,
          time_max: time_max
        )
      rescue Google::Apis::Error => e
        warn Locale.t("errors.calendar_fetch", calendar: calendar_id, message: e.message)
        []
      end

      finder = SlotFinder.new(
        events: events,
        time_min: time_min,
        time_max: time_max,
        slot_duration_seconds: slot_duration,
        hours: hours,
        days: days,
        free_before_seconds: free_before,
        free_after_seconds: free_after
      )

      slots_by_day = finder.slots_by_day

      output =
        if format == "json"
          SlotFormatter.format_json(
            slots_by_day: slots_by_day,
            time_min: time_min,
            time_max: time_max,
            duration: duration_value,
            template: template_name,
            calendars: calendar_meta,
            locale: Locale.current_locale,
            timezone: timezone,
            calendar_option: options[:calendar],
            from_option: options[:from],
            in_option: options[:in],
            free_before: slot_cfg["free_before"],
            free_after: slot_cfg["free_after"],
            empty_message: Locale.t("slots.no_available")
          )
      else
        SlotFormatter.format_text(
          slots_by_day: slots_by_day,
          time_min: time_min,
          time_max: time_max,
          duration: duration_value,
          calendars: calendar_meta,
          template: template_name,
          empty_message: Locale.t("slots.no_available")
        )
      end

      FileCache.write(cache_key, strip_cache_marker_from_output(output, format))
      status = "MISS"
    end
  end

  print annotate_cli_output(output, status, cache_key, format)

  if format == "text"
    body = extract_slots_body_for_clipboard(output)
    copy_slots_to_clipboard(body) unless body.empty?
  end
end

#updateObject



491
492
493
494
495
496
497
498
499
# File 'lib/flycal/cli/app.rb', line 491

def update
  apply_locale_override
  puts "Updating flycal-cli..."
  success = system("gem", "update", "flycal-cli")
  return if success

  puts "Error: failed to update flycal-cli."
  exit 1
end

#versionObject



502
503
504
505
# File 'lib/flycal/cli/app.rb', line 502

def version
  apply_locale_override
  puts "flycal #{Flycal::VERSION}"
end