flycal-cli 0.7.7 → 1.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -181
  3. data/bin/flycal +2 -2
  4. data/config/defaults.yml +2 -28
  5. data/lib/flycal/cache_annotator.rb +83 -0
  6. data/lib/flycal/cache_key.rb +82 -0
  7. data/lib/flycal/calendar_query.rb +60 -0
  8. data/lib/{flycal_cli → flycal}/calendar_service.rb +48 -4
  9. data/lib/{flycal_cli/cli.rb → flycal/cli/app.rb} +246 -171
  10. data/lib/{flycal_cli → flycal/cli}/auth.rb +5 -3
  11. data/lib/{flycal_cli → flycal/cli}/clipboard.rb +3 -1
  12. data/lib/{flycal_cli → flycal/cli}/config.rb +6 -3
  13. data/lib/flycal/cli/file_cache.rb +44 -0
  14. data/lib/flycal/cli/locale.rb +58 -0
  15. data/lib/flycal/cli.rb +14 -0
  16. data/lib/flycal/client.rb +141 -0
  17. data/lib/flycal/credentials.rb +34 -0
  18. data/lib/{flycal_cli → flycal}/date_time_parser.rb +4 -4
  19. data/lib/flycal/default_slots.rb +77 -0
  20. data/lib/{flycal_cli → flycal}/description_query.rb +3 -3
  21. data/lib/{flycal_cli → flycal}/duration_parser.rb +3 -3
  22. data/lib/flycal/error.rb +5 -0
  23. data/lib/flycal/event_mapper.rb +58 -0
  24. data/lib/{flycal_cli → flycal}/locale.rb +35 -3
  25. data/lib/{flycal_cli → flycal}/mock/calendar_service.rb +1 -1
  26. data/lib/{flycal_cli → flycal}/mock/config.rb +14 -14
  27. data/lib/{flycal_cli → flycal}/mock/event_generator.rb +6 -4
  28. data/lib/flycal/mock/slot_scenario.rb +75 -0
  29. data/lib/flycal/mock.rb +6 -0
  30. data/lib/{flycal_cli → flycal}/pipeline/aggregator.rb +21 -24
  31. data/lib/{flycal_cli → flycal}/pipeline/json_renderer.rb +16 -17
  32. data/lib/{flycal_cli → flycal}/pipeline/params.rb +1 -1
  33. data/lib/{flycal_cli → flycal}/pipeline/renderer.rb +4 -4
  34. data/lib/{flycal_cli → flycal}/pipeline/retriever.rb +1 -1
  35. data/lib/{flycal_cli → flycal}/pipeline/search_pipeline.rb +1 -1
  36. data/lib/{flycal_cli → flycal}/pipeline/text_renderer.rb +14 -12
  37. data/lib/flycal/pipeline.rb +6 -0
  38. data/lib/{flycal_cli → flycal}/slot_finder.rb +12 -12
  39. data/lib/flycal/slot_formatter.rb +196 -0
  40. data/lib/flycal/slot_query.rb +74 -0
  41. data/lib/flycal/slot_templates.rb +129 -0
  42. data/lib/flycal/time_format.rb +36 -0
  43. data/lib/flycal/version.rb +11 -0
  44. data/lib/flycal.rb +12 -0
  45. data/lib/flycal_cli.rb +42 -17
  46. data/mcp/tools.json +21 -8
  47. metadata +41 -25
  48. data/lib/flycal_cli/mock.rb +0 -10
  49. data/lib/flycal_cli/pipeline.rb +0 -14
  50. data/lib/flycal_cli/slot_formatter.rb +0 -68
  51. data/lib/flycal_cli/version.rb +0 -5
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/blank"
4
+ require "active_support/core_ext/hash/indifferent_access"
5
+
6
+ module Flycal
7
+ # Resolves slots search params (template, window, duration) without calling Google.
8
+ class SlotQuery
9
+ class << self
10
+ def resolve(params = {})
11
+ p = params.respond_to?(:with_indifferent_access) ? params.with_indifferent_access : params
12
+ calendar_ids = Array(p[:calendar_ids]).map(&:to_s).reject(&:empty?)
13
+ raise Error, "calendar_ids are required" if calendar_ids.empty?
14
+
15
+ duration_value = p[:duration].presence || "45min"
16
+ from_value = p[:from].presence || "now"
17
+ free_before_value = p[:free_before].presence || "0m"
18
+ free_after_value = p[:free_after].presence || "0m"
19
+ locale = p[:locale].presence || Locale.current_locale
20
+
21
+ template = SlotTemplates.resolve(
22
+ name: p[:template],
23
+ custom: p[:template_custom],
24
+ catalog: p[:template_catalog]
25
+ )
26
+
27
+ slot_duration = DurationParser.to_seconds(duration_value)
28
+ free_before = DurationParser.to_seconds(free_before_value)
29
+ free_after = DurationParser.to_seconds(free_after_value)
30
+ time_min = DateTimeParser.parse(from_value.to_s, locale: locale)
31
+ time_max = resolve_time_max(p, time_min, locale)
32
+ raise Error, "'from' must be before end of window" if time_min >= time_max
33
+
34
+ hours = p[:hours].present? ? SlotTemplates.parse_hours(p[:hours]) : template[:hours]
35
+ days = p[:days].present? ? SlotTemplates.parse_days(p[:days]) : template[:days]
36
+ in_value = p[:in].presence
37
+
38
+ {
39
+ command: "slots",
40
+ calendar_ids: calendar_ids,
41
+ duration: duration_value,
42
+ from: from_value,
43
+ to: p[:to],
44
+ in: in_value,
45
+ free_before: free_before_value,
46
+ free_after: free_after_value,
47
+ template: template[:name],
48
+ locale: locale,
49
+ calendar: p[:calendar],
50
+ hours: hours,
51
+ days: days,
52
+ time_min: time_min,
53
+ time_max: time_max,
54
+ slot_duration_seconds: slot_duration,
55
+ free_before_seconds: free_before,
56
+ free_after_seconds: free_after,
57
+ format: p[:format].presence || "json"
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ def resolve_time_max(params, time_min, locale)
64
+ if params[:in].present?
65
+ DurationParser.add_to_time(params[:in], time_min)
66
+ elsif params[:to].present?
67
+ DateTimeParser.parse(params[:to].to_s, end_of_day: true, locale: locale)
68
+ else
69
+ DurationParser.add_to_time("1 week", time_min)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flycal
4
+ # Named and custom slot windows. YAML/JSON shape matches ~/.flycal/config.yml:
5
+ #
6
+ # days: [1, 2, 3, 4, 5]
7
+ # hours:
8
+ # - 9:30-13:00
9
+ # - 14:00-18:30
10
+ module SlotTemplates
11
+ BUILTIN = {
12
+ "work" => {
13
+ "days" => [ 1, 2, 3, 4, 5 ],
14
+ "hours" => [ "9:30-13:00", "14:00-18:30" ]
15
+ },
16
+ "dinner" => {
17
+ "days" => [ 1, 2, 3, 4, 5, 6 ],
18
+ "hours" => [ "19-23" ]
19
+ }
20
+ }.freeze
21
+
22
+ module_function
23
+
24
+ def names(catalog = nil)
25
+ normalize_catalog(catalog || DefaultSlots.templates).keys
26
+ end
27
+
28
+ # Resolve a named template, or a custom YAML-shaped hash.
29
+ # +custom+ wins over +name+ when present.
30
+ def resolve(name: nil, custom: nil, catalog: nil)
31
+ if custom_present?(custom)
32
+ parsed = parse(custom)
33
+ return parsed.merge(name: "custom")
34
+ end
35
+
36
+ source = normalize_catalog(catalog || DefaultSlots.templates)
37
+ raise Error, "slots.templates is missing." if source.empty?
38
+ key = name.to_s.strip
39
+ key = source.keys.first.to_s if key.empty?
40
+ definition = source[key]
41
+ unless definition
42
+ raise Error, "Unknown slots template #{key.inspect}. Available: #{source.keys.join(", ")}"
43
+ end
44
+
45
+ parse(definition).merge(name: key)
46
+ end
47
+
48
+ def parse(definition)
49
+ data = indifferent(definition)
50
+ days = parse_days(data[:days])
51
+ hours = parse_hours(data[:hours])
52
+ { days: days, hours: hours }
53
+ end
54
+
55
+ def parse_days(values)
56
+ days = Array(values).map(&:to_i)
57
+ raise Error, "slots template days cannot be empty." if days.empty?
58
+
59
+ invalid = days.reject { |d| d.between?(1, 7) }
60
+ raise Error, "Invalid template days #{invalid.inspect}. Use 1 (Mon) .. 7 (Sun)." unless invalid.empty?
61
+
62
+ days.uniq
63
+ end
64
+
65
+ def parse_hours(values)
66
+ return values if values.is_a?(Array) && values.first.is_a?(Array)
67
+
68
+ ranges = Array(values).map { |item| parse_hour_range(item) }
69
+ raise Error, "template hours cannot be empty." if ranges.empty?
70
+
71
+ ranges.sort_by { |sh, sm, _eh, _em| (sh * 60) + sm }
72
+ end
73
+
74
+ def parse_hour_range(item)
75
+ start_str, end_str = item.to_s.strip.split("-", 2)
76
+ if start_str.nil? || end_str.nil? || start_str.empty? || end_str.empty?
77
+ raise Error, "Invalid hours item #{item.inspect}. Use format like '9-13' or '14:30-18:00'."
78
+ end
79
+
80
+ sh, sm = parse_clock(start_str)
81
+ eh, em = parse_clock(end_str)
82
+ start_minutes = (sh * 60) + sm
83
+ end_minutes = (eh * 60) + em
84
+ raise Error, "Invalid hours range #{item.inspect}: end must be after start." if end_minutes <= start_minutes
85
+
86
+ [ sh, sm, eh, em ]
87
+ end
88
+
89
+ def parse_clock(value)
90
+ str = value.to_s.strip
91
+ if str.match?(/\A\d{1,2}\z/)
92
+ hour = str.to_i
93
+ raise Error, "Invalid hour #{value.inspect}. Must be 0-23." unless hour.between?(0, 23)
94
+
95
+ return [ hour, 0 ]
96
+ end
97
+
98
+ hour_s, minute_s = str.split(":", 2)
99
+ hour = hour_s.to_i
100
+ minute = minute_s.to_i
101
+ unless hour.between?(0, 23) && minute.between?(0, 59)
102
+ raise Error, "Invalid time #{value.inspect}. Use HH:MM (e.g. 9:00, 18:30)."
103
+ end
104
+
105
+ [ hour, minute ]
106
+ end
107
+
108
+ def custom_present?(custom)
109
+ return false if custom.nil?
110
+ return false if custom.respond_to?(:blank?) && custom.blank?
111
+ return false if custom.respond_to?(:empty?) && custom.empty?
112
+
113
+ true
114
+ end
115
+
116
+ def normalize_catalog(catalog)
117
+ indifferent(catalog).each_with_object({}) do |(key, value), out|
118
+ out[key.to_s] = value
119
+ end
120
+ end
121
+
122
+ def indifferent(value)
123
+ return {} if value.nil?
124
+ return value.with_indifferent_access if value.respond_to?(:with_indifferent_access)
125
+
126
+ value
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "date"
5
+
6
+ module Flycal
7
+ # Single clock for every datetime Flycal emits (JSON + text).
8
+ #
9
+ # Instants are converted to process-local Time so start/end/from/to never mix
10
+ # offsets (e.g. +02:00 vs +00:00) in the same payload.
11
+ module TimeFormat
12
+ module_function
13
+
14
+ def iso8601(value)
15
+ time = unified(value)
16
+ time&.iso8601
17
+ end
18
+
19
+ def unified(value)
20
+ time = coerce(value)
21
+ return nil unless time
22
+
23
+ Time.at(time.to_f)
24
+ end
25
+
26
+ def coerce(value)
27
+ return nil if value.nil?
28
+ return value if value.is_a?(Time)
29
+ return value.to_time if value.respond_to?(:to_time)
30
+
31
+ Time.parse(value.to_s)
32
+ rescue ArgumentError
33
+ nil
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flycal
4
+ # Kept for Zeitwerk (`lib/flycal/version.rb` → Flycal::Version).
5
+ # Canonical version string lives on Flycal::VERSION (see lib/flycal.rb).
6
+ module Version
7
+ def self.to_s
8
+ Flycal::VERSION
9
+ end
10
+ end
11
+ end
data/lib/flycal.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Flycal core namespace.
4
+ # Under Rails, Zeitwerk autoloads lib/flycal/*.
5
+ # The CLI gem entrypoint (flycal_cli/lib/flycal_cli.rb) eager-requires what it needs.
6
+ module Flycal
7
+ VERSION = "1.1"
8
+
9
+ def self.connect(credentials:)
10
+ Client.new(credentials: credentials)
11
+ end
12
+ end
data/lib/flycal_cli.rb CHANGED
@@ -1,20 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "flycal_cli/version"
4
- require "flycal_cli/config"
5
- require "flycal_cli/locale"
6
- require "flycal_cli/auth"
7
- require "flycal_cli/duration_parser"
8
- require "flycal_cli/date_time_parser"
9
- require "flycal_cli/clipboard"
10
- require "flycal_cli/description_query"
11
- require "flycal_cli/calendar_service"
12
- require "flycal_cli/slot_finder"
13
- require "flycal_cli/slot_formatter"
14
- require "flycal_cli/mock"
15
- require "flycal_cli/pipeline"
16
- require "flycal_cli/cli"
3
+ # Gem entrypoint for the flycal-cli package.
4
+ core_lib = File.expand_path("../../lib", __dir__)
5
+ $LOAD_PATH.unshift(core_lib) unless $LOAD_PATH.include?(core_lib)
6
+ cli_lib = File.expand_path(".", __dir__)
7
+ $LOAD_PATH.unshift(cli_lib) unless $LOAD_PATH.include?(cli_lib)
17
8
 
18
- module FlycalCli
19
- class Error < StandardError; end
20
- end
9
+ # Eager-load Flycal core (outside Rails/Zeitwerk).
10
+ require "flycal/version"
11
+ require "flycal/error"
12
+ require "flycal/locale"
13
+ require "flycal/time_format"
14
+ require "flycal/credentials"
15
+ require "flycal/event_mapper"
16
+ require "flycal/cache_key"
17
+ require "flycal/cache_annotator"
18
+ require "flycal/client"
19
+ require "flycal/duration_parser"
20
+ require "flycal/date_time_parser"
21
+ require "flycal/description_query"
22
+ require "flycal/calendar_query"
23
+ require "flycal/calendar_service"
24
+ require "flycal/slot_finder"
25
+ require "flycal/slot_templates"
26
+ require "flycal/default_slots"
27
+ require "flycal/slot_query"
28
+ require "flycal/slot_formatter"
29
+ require "flycal/mock"
30
+ require "flycal/mock/config"
31
+ require "flycal/mock/event_generator"
32
+ require "flycal/mock/calendar_service"
33
+ require "flycal/mock/slot_scenario"
34
+ require "flycal/pipeline"
35
+ require "flycal/pipeline/params"
36
+ require "flycal/pipeline/retriever"
37
+ require "flycal/pipeline/aggregator"
38
+ require "flycal/pipeline/renderer"
39
+ require "flycal/pipeline/text_renderer"
40
+ require "flycal/pipeline/json_renderer"
41
+ require "flycal/pipeline/search_pipeline"
42
+
43
+ require "flycal/cli"
44
+
45
+ FlycalCli = Flycal::Cli unless defined?(FlycalCli)
data/mcp/tools.json CHANGED
@@ -461,7 +461,7 @@
461
461
  {
462
462
  "name": "flycal_slots",
463
463
  "title": "Find free time slots",
464
- "description": "Find continuous free ranges using a slots.templates schedule (days + hours). Default template = first key (usually work). Busy events come from slots.exclude_calendars (or calendar_default if empty). Respects free_before/free_after. Prints header + availability; copies availability list to clipboard when a clipboard tool exists.",
464
+ "description": "Find continuous free ranges using a slots.templates schedule (days + hours). Default template = first key (usually work). Busy events come from slots.exclude_calendars (or calendar_default if empty). Respects free_before/free_after. Output: text (header + availability, clipboard) or JSON via --format json.",
465
465
  "interactive": false,
466
466
  "requires_auth": true,
467
467
  "command": {
@@ -473,14 +473,16 @@
473
473
  "in": ["--in", "{value}"],
474
474
  "template": ["--template", "{value}"],
475
475
  "calendar": ["--calendar", "{value}"],
476
- "locale": ["--locale", "{value}"]
476
+ "locale": ["--locale", "{value}"],
477
+ "format": ["--format", "{value}"]
477
478
  },
478
479
  "examples": [
479
480
  ["flycal", "slots"],
480
481
  ["flycal", "slots", "--in", "5 days"],
481
482
  ["flycal", "slots", "--from", "monday", "--in", "5 days", "--locale", "it"],
482
483
  ["flycal", "slots", "--in", "12 days", "--template", "dinner"],
483
- ["flycal", "slots", "--from", "next monday", "--duration", "1h", "--template", "work"]
484
+ ["flycal", "slots", "--from", "next monday", "--duration", "1h", "--template", "work"],
485
+ ["flycal", "slots", "--format", "json", "--in", "5 days"]
484
486
  ]
485
487
  },
486
488
  "parameters": {
@@ -532,15 +534,21 @@
532
534
  "enum": ["en", "it"],
533
535
  "required": false,
534
536
  "description": "Optional locale override."
537
+ },
538
+ "format": {
539
+ "cli": ["--format"],
540
+ "type": "string",
541
+ "enum": ["text", "json"],
542
+ "required": false,
543
+ "default": "text",
544
+ "description": "Output format: text (shell + clipboard) or json (params/info/items/groups)."
535
545
  }
536
546
  },
537
547
  "output": {
538
- "format": "text/plain",
548
+ "format": "text/plain or application/json",
539
549
  "includes": [
540
- "count + from/to + duration + template",
541
- "bullet list of considered calendars",
542
- "link: Google Calendar day URL at --from",
543
- "availability ranges grouped by day (copied to clipboard without header)"
550
+ "text: count + from/to + duration + template, calendars, link, ranges by day (clipboard)",
551
+ "json: params, info (slots_found, calendars, link), flat items, groups by day"
544
552
  ],
545
553
  "example": "found 6 slots\nfrom sat 1 August 2026 to sat 8 August 2026\nwith duration 45min, template dinner\nconsidering calendars\n- Work\n- Personal\nlink: https://calendar.google.com/calendar/r/day/2026/8/1\n\nsaturday 1/8\n19 - 23"
546
554
  },
@@ -573,6 +581,11 @@
573
581
  "type": "string",
574
582
  "enum": ["en", "it"],
575
583
  "description": "--locale. Optional locale override (en, it)."
584
+ },
585
+ "format": {
586
+ "type": "string",
587
+ "enum": ["text", "json"],
588
+ "description": "--format. text (default) or json."
576
589
  }
577
590
  }
578
591
  }
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.7.7
4
+ version: '1.1'
5
5
  platform: ruby
6
6
  authors:
7
7
  - flycal-cli
@@ -134,31 +134,47 @@ files:
134
134
  - README.md
135
135
  - bin/flycal
136
136
  - config/defaults.yml
137
+ - lib/flycal.rb
138
+ - lib/flycal/cache_annotator.rb
139
+ - lib/flycal/cache_key.rb
140
+ - lib/flycal/calendar_query.rb
141
+ - lib/flycal/calendar_service.rb
142
+ - lib/flycal/cli.rb
143
+ - lib/flycal/cli/app.rb
144
+ - lib/flycal/cli/auth.rb
145
+ - lib/flycal/cli/clipboard.rb
146
+ - lib/flycal/cli/config.rb
147
+ - lib/flycal/cli/file_cache.rb
148
+ - lib/flycal/cli/locale.rb
149
+ - lib/flycal/client.rb
150
+ - lib/flycal/credentials.rb
151
+ - lib/flycal/date_time_parser.rb
152
+ - lib/flycal/default_slots.rb
153
+ - lib/flycal/description_query.rb
154
+ - lib/flycal/duration_parser.rb
155
+ - lib/flycal/error.rb
156
+ - lib/flycal/event_mapper.rb
157
+ - lib/flycal/locale.rb
158
+ - lib/flycal/mock.rb
159
+ - lib/flycal/mock/calendar_service.rb
160
+ - lib/flycal/mock/config.rb
161
+ - lib/flycal/mock/event_generator.rb
162
+ - lib/flycal/mock/slot_scenario.rb
163
+ - lib/flycal/pipeline.rb
164
+ - lib/flycal/pipeline/aggregator.rb
165
+ - lib/flycal/pipeline/json_renderer.rb
166
+ - lib/flycal/pipeline/params.rb
167
+ - lib/flycal/pipeline/renderer.rb
168
+ - lib/flycal/pipeline/retriever.rb
169
+ - lib/flycal/pipeline/search_pipeline.rb
170
+ - lib/flycal/pipeline/text_renderer.rb
171
+ - lib/flycal/slot_finder.rb
172
+ - lib/flycal/slot_formatter.rb
173
+ - lib/flycal/slot_query.rb
174
+ - lib/flycal/slot_templates.rb
175
+ - lib/flycal/time_format.rb
176
+ - lib/flycal/version.rb
137
177
  - lib/flycal_cli.rb
138
- - lib/flycal_cli/auth.rb
139
- - lib/flycal_cli/calendar_service.rb
140
- - lib/flycal_cli/cli.rb
141
- - lib/flycal_cli/clipboard.rb
142
- - lib/flycal_cli/config.rb
143
- - lib/flycal_cli/date_time_parser.rb
144
- - lib/flycal_cli/description_query.rb
145
- - lib/flycal_cli/duration_parser.rb
146
- - lib/flycal_cli/locale.rb
147
- - lib/flycal_cli/mock.rb
148
- - lib/flycal_cli/mock/calendar_service.rb
149
- - lib/flycal_cli/mock/config.rb
150
- - lib/flycal_cli/mock/event_generator.rb
151
- - lib/flycal_cli/pipeline.rb
152
- - lib/flycal_cli/pipeline/aggregator.rb
153
- - lib/flycal_cli/pipeline/json_renderer.rb
154
- - lib/flycal_cli/pipeline/params.rb
155
- - lib/flycal_cli/pipeline/renderer.rb
156
- - lib/flycal_cli/pipeline/retriever.rb
157
- - lib/flycal_cli/pipeline/search_pipeline.rb
158
- - lib/flycal_cli/pipeline/text_renderer.rb
159
- - lib/flycal_cli/slot_finder.rb
160
- - lib/flycal_cli/slot_formatter.rb
161
- - lib/flycal_cli/version.rb
162
178
  - locales/en.json
163
179
  - locales/it.json
164
180
  - mcp/README.md
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "flycal_cli/mock/config"
4
- require "flycal_cli/mock/event_generator"
5
- require "flycal_cli/mock/calendar_service"
6
-
7
- module FlycalCli
8
- module Mock
9
- end
10
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "flycal_cli/pipeline/params"
4
- require "flycal_cli/pipeline/retriever"
5
- require "flycal_cli/pipeline/aggregator"
6
- require "flycal_cli/pipeline/renderer"
7
- require "flycal_cli/pipeline/text_renderer"
8
- require "flycal_cli/pipeline/json_renderer"
9
- require "flycal_cli/pipeline/search_pipeline"
10
-
11
- module FlycalCli
12
- module Pipeline
13
- end
14
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FlycalCli
4
- class SlotFormatter
5
- class << self
6
- def format_header(from:, to:, duration:, calendars:, count:, template: nil)
7
- lines = []
8
- lines << Locale.t(
9
- "slots.header",
10
- count: underline(count),
11
- from: underline(Locale.format_long_date(from)),
12
- to: underline(Locale.format_long_date(to)),
13
- duration: underline(duration),
14
- template: underline(template.to_s)
15
- )
16
- calendars.each do |cal|
17
- lines << "- #{cal[:name]}"
18
- end
19
- lines << "link: #{google_calendar_day_url(from)}"
20
- lines.join("\n")
21
- end
22
-
23
- def format_output(slots_by_day)
24
- lines = []
25
-
26
- slots_by_day.sort_by(&:first).each do |date, slots|
27
- lines << "" unless lines.empty?
28
- lines << underline(day_header(date))
29
- slots.each do |start_at, end_at|
30
- lines << slot_range(start_at, end_at)
31
- end
32
- end
33
-
34
- lines.join("\n")
35
- end
36
-
37
- def strip_ansi(text)
38
- text.to_s.gsub(/\e\[[0-9;]*m/, "")
39
- end
40
-
41
- private
42
-
43
- def underline(str)
44
- "\e[4m#{str}\e[0m"
45
- end
46
-
47
- def google_calendar_day_url(from)
48
- t = from.respond_to?(:to_time) ? from.to_time : from
49
- "https://calendar.google.com/calendar/r/day/#{t.year}/#{t.month}/#{t.day}"
50
- end
51
-
52
- def day_header(date)
53
- "#{Locale.day_name(date)} #{date.day}/#{date.month}"
54
- end
55
-
56
- def slot_range(start_at, end_at)
57
- "#{format_time(start_at)} - #{format_time(end_at)}"
58
- end
59
-
60
- def format_time(time)
61
- return time.strftime("%-H") if time.min.zero?
62
-
63
- minutes = sprintf("%02d", time.min)
64
- "#{time.hour}.#{minutes}"
65
- end
66
- end
67
- end
68
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FlycalCli
4
- VERSION = '0.7.7'
5
- end