flycal-cli 0.7.0 → 0.7.2

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: a0ee7fffad246d3a457d03eea4af9ace9bb6dfe59a067cc62bf2da08987a72a3
4
- data.tar.gz: c9a5ae5776356185d80f6e87b368b6027122b27daeeb03378f01b6000486b16a
3
+ metadata.gz: 371b44577d9ce6befe5b01bc54768999844615943b982a6e9cd9cd8c9737d007
4
+ data.tar.gz: e3c6c00e03e38eccaea97ca045582a35d737916f1203963cc2c989ebf75de46b
5
5
  SHA512:
6
- metadata.gz: dd15a9fc4be4bb6da73354dc17e600fdbcc19701c9e13668cbf504cdc99bd568a4f10a8d429a39e6ca41d9d4cb77a84501843e2c5820c959f1680a64384d1368
7
- data.tar.gz: 708402193736049bc5b1ba476d765326ebf5d92d53eef48d12cb81f62b3d93b2a98db6913de535c8b9fdb26922b03f9b487f8e2192f18beb4037ad6c8cc423b3
6
+ metadata.gz: 35d47be3955556a98d3301782c2fc7deb7064134cb38792f128ade6c78807d5f28131696d14b3f2a72c7d7ae5acf9764b880d634ac52665accc6aa239c1026fe
7
+ data.tar.gz: da10a2548c8bdd03ebc94e990b58e7a3d30de98e2f759a927e777e08d8617006713abeafd9e220fac4fbbb6d9c1e0835a8e0cb8650f4ae30f4a00a355bb1f478
data/README.md CHANGED
@@ -157,16 +157,18 @@ Find free time slots in your calendar. Output is a simple list for copy/paste in
157
157
 
158
158
  ```bash
159
159
  flycal slots
160
- flycal slots --in "5 days"
160
+ flycal slots --from monday --in "5 days"
161
+ flycal slots --from "next monday" --in "5 days" --locale it
162
+ flycal slots --from lunedi --in "5 days" --locale it
161
163
  flycal slots --in "12 days" --template dinner
162
- flycal slots --duration 1h
163
- flycal slots --from 2026-08-01 --in "2 weeks" --duration 1h
164
164
  ```
165
165
 
166
166
  **Options:**
167
167
 
168
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.
169
+ - `--from` / `-f` — Start date/time. Default: `slots.defaults.from` in config (`now`). Accepts:
170
+ - absolute: `YYYY-MM-DD`, locale dates (`DD-MM-YYYY` / `MM-DD-YYYY`), optional time
171
+ - relative: `now`, `today`/`oggi`, `tomorrow`/`domani`, `monday`/`lunedi`, `next monday`/`prossimo lunedi`, `last friday`/`scorso venerdi`
170
172
  - `--in` / `-i` — Search window from `--from`. Default: `1 week`.
171
173
  - `--template` / `-T` — Template from `slots.templates` in config. Default: first template (`work`)
172
174
  - `--calendar` / `-c` — Calendar name or ID used as fallback when `exclude_calendars` is not configured
@@ -174,18 +176,20 @@ flycal slots --from 2026-08-01 --in "2 weeks" --duration 1h
174
176
  **Output:**
175
177
 
176
178
  ```
177
- found 3 slots
178
- from wed 29 July 2026 to wed 5 August 2026
179
- with duration 45min
180
- for calendars
181
- template work
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
179
+ found 6 slots
180
+ from sat 1 August 2026 to sat 8 August 2026
181
+ with duration 45min, template dinner
182
+ considering calendars
183
+ - incode - antonio
184
+ - Polimi 10110009
185
+ - antoniomolinari1977@gmail.com
186
+ link: https://calendar.google.com/calendar/r/day/2026/8/1
187
+
188
+ saturday 1/8
189
+ 19 - 23
190
+
191
+ monday 3/8
192
+ 19 - 23
189
193
  ```
190
194
 
191
195
  If `pbcopy` is available (macOS), the slot list without the header is also copied to the clipboard.
@@ -195,7 +195,7 @@ module FlycalCli
195
195
  option :in, type: :string, aliases: "-i", default: "1 week",
196
196
  desc: "Search window from --from (default: 1 week)"
197
197
  option :from, type: :string, aliases: "-f",
198
- desc: "Start date/time (default from config: slots.defaults.from)"
198
+ desc: "Start (default: now). Dates, or relative: monday, next monday, tomorrow, lunedi..."
199
199
  option :template, type: :string, aliases: "-T",
200
200
  desc: "Slot template name from config (default: first template, usually work)"
201
201
  option :calendar, type: :string, aliases: "-c", desc: "Calendar name or ID"
@@ -507,7 +507,7 @@ module FlycalCli
507
507
  def copy_slots_to_clipboard(text)
508
508
  return unless pbcopy_available?
509
509
 
510
- IO.popen("pbcopy", "w") { |io| io.write(text) }
510
+ IO.popen("pbcopy", "w") { |io| io.write(SlotFormatter.strip_ansi(text)) }
511
511
  puts ""
512
512
  puts Locale.t("slots.copied_to_clipboard")
513
513
  rescue StandardError
@@ -12,16 +12,38 @@ module FlycalCli
12
12
  # YYYY-MM-DD, YYYY/MM/DD
13
13
  # YYYY-MM-DDTHH:MM, YYYY-MM-DD HH:MM(:SS)
14
14
  #
15
+ # Relative forms (en + it):
16
+ # now, today/oggi, tomorrow/domani, yesterday/ieri
17
+ # monday / lunedi, next monday / prossimo lunedi, last friday / scorso venerdi
18
+ #
15
19
  # With locale +it+: DD-MM-YYYY / DD/MM/YYYY (+ optional time)
16
20
  # With locale +en+ (default): MM-DD-YYYY / MM/DD/YYYY (+ optional time)
17
21
  class DateTimeParser
18
22
  TIME_SUFFIX = '(?:\s+|T)(\d{1,2}):(\d{2})(?::(\d{2}))?'.freeze
19
23
 
24
+ # Ruby Date#wday: 0=Sunday ... 6=Saturday
25
+ WEEKDAYS = {
26
+ "sunday" => 0, "sun" => 0, "domenica" => 0, "dom" => 0,
27
+ "monday" => 1, "mon" => 1, "lunedi" => 1, "lunedì" => 1, "lun" => 1,
28
+ "tuesday" => 2, "tue" => 2, "martedi" => 2, "martedì" => 2, "mar" => 2,
29
+ "wednesday" => 3, "wed" => 3, "mercoledi" => 3, "mercoledì" => 3, "mer" => 3,
30
+ "thursday" => 4, "thu" => 4, "giovedi" => 4, "giovedì" => 4, "gio" => 4,
31
+ "friday" => 5, "fri" => 5, "venerdi" => 5, "venerdì" => 5, "ven" => 5,
32
+ "saturday" => 6, "sat" => 6, "sabato" => 6, "sab" => 6
33
+ }.freeze
34
+
35
+ NEXT_WORDS = %w[next prossimo prossima].freeze
36
+ LAST_WORDS = %w[last scorso scorsa].freeze
37
+ THIS_WORDS = %w[this questo questa].freeze
38
+
20
39
  class << self
21
40
  def parse(str, end_of_day: false, locale: nil)
22
41
  value = str.to_s.strip
23
42
  raise FlycalCli::Error, invalid_message(str) if value.empty?
24
43
 
44
+ relative = parse_relative(value, end_of_day: end_of_day)
45
+ return relative if relative
46
+
25
47
  loc = (locale || Locale.current_locale).to_s
26
48
  formats = formats_for(loc)
27
49
 
@@ -33,7 +55,6 @@ module FlycalCli
33
55
  return build_time(year, month, day, hour, min, sec, end_of_day: end_of_day)
34
56
  end
35
57
 
36
- # Last resort: ISO8601 / Time.parse for full timestamps
37
58
  begin
38
59
  return Time.iso8601(value)
39
60
  rescue ArgumentError
@@ -56,6 +77,76 @@ module FlycalCli
56
77
 
57
78
  private
58
79
 
80
+ def parse_relative(value, end_of_day:)
81
+ normalized = value.downcase.strip
82
+ normalized = normalized.tr("àèéìòù", "aeeiou")
83
+
84
+ case normalized
85
+ when "now", "adesso", "ora"
86
+ return Time.now
87
+ when "today", "oggi"
88
+ return time_for_date(Date.today, end_of_day: end_of_day)
89
+ when "tomorrow", "domani"
90
+ return time_for_date(Date.today + 1, end_of_day: end_of_day)
91
+ when "yesterday", "ieri"
92
+ return time_for_date(Date.today - 1, end_of_day: end_of_day)
93
+ end
94
+
95
+ weekday = parse_weekday_phrase(normalized)
96
+ return nil unless weekday
97
+
98
+ time_for_date(weekday, end_of_day: end_of_day)
99
+ end
100
+
101
+ def parse_weekday_phrase(normalized)
102
+ tokens = normalized.split(/\s+/)
103
+ return nil if tokens.empty?
104
+
105
+ qualifier = nil
106
+ day_token = nil
107
+
108
+ if tokens.length == 1
109
+ day_token = tokens[0]
110
+ elsif tokens.length == 2
111
+ qualifier = tokens[0]
112
+ day_token = tokens[1]
113
+ else
114
+ return nil
115
+ end
116
+
117
+ wday = WEEKDAYS[day_token]
118
+ return nil unless wday
119
+
120
+ today = Date.today
121
+
122
+ if qualifier.nil? || THIS_WORDS.include?(qualifier)
123
+ next_weekday(today, wday, inclusive: true)
124
+ elsif NEXT_WORDS.include?(qualifier)
125
+ next_weekday(today, wday, inclusive: false)
126
+ elsif LAST_WORDS.include?(qualifier)
127
+ previous_weekday(today, wday, inclusive: false)
128
+ end
129
+ end
130
+
131
+ def next_weekday(from, wday, inclusive:)
132
+ delta = (wday - from.wday) % 7
133
+ delta = 7 if delta.zero? && !inclusive
134
+ from + delta
135
+ end
136
+
137
+ def previous_weekday(from, wday, inclusive:)
138
+ delta = (from.wday - wday) % 7
139
+ delta = 7 if delta.zero? && !inclusive
140
+ from - delta
141
+ end
142
+
143
+ def time_for_date(date, end_of_day:)
144
+ return end_of_day_for(date) if end_of_day
145
+ return [date.to_time, Time.now].max if date == Date.today
146
+
147
+ date.to_time
148
+ end
149
+
59
150
  def formats_for(locale)
60
151
  iso = [
61
152
  [/\A(\d{4})[-\/](\d{1,2})[-\/](\d{1,2})#{TIME_SUFFIX}\z/i, %i[year month day hour min sec]],
@@ -98,10 +189,7 @@ module FlycalCli
98
189
  date = Date.new(year, month, day)
99
190
 
100
191
  if hour.nil?
101
- return end_of_day_for(date) if end_of_day
102
- return [date.to_time, Time.now].max if date == Date.today
103
-
104
- return date.to_time
192
+ return time_for_date(date, end_of_day: end_of_day)
105
193
  end
106
194
 
107
195
  Time.local(year, month, day, hour, min || 0, sec || 0)
@@ -119,9 +207,9 @@ module FlycalCli
119
207
 
120
208
  def invalid_message(str)
121
209
  if Locale.current_locale.to_s == "it"
122
- "Data non valida: #{str.inspect}. Usa YYYY-MM-DD, DD-MM-YYYY (anche con /) o con orario."
210
+ "Data non valida: #{str.inspect}. Usa YYYY-MM-DD, DD-MM-YYYY, oggi/domani, lunedi, prossimo lunedi, ecc."
123
211
  else
124
- "Invalid date: #{str.inspect}. Use YYYY-MM-DD, MM-DD-YYYY (also with /), or with time."
212
+ "Invalid date: #{str.inspect}. Use YYYY-MM-DD, MM-DD-YYYY, today/tomorrow, monday, next monday, etc."
125
213
  end
126
214
  end
127
215
  end
@@ -7,16 +7,16 @@ module FlycalCli
7
7
  lines = []
8
8
  lines << Locale.t(
9
9
  "slots.header",
10
- count: count,
10
+ count: underline(count),
11
11
  from: underline(Locale.format_long_date(from)),
12
12
  to: underline(Locale.format_long_date(to)),
13
- duration: underline(duration)
13
+ duration: underline(duration),
14
+ template: underline(template.to_s)
14
15
  )
15
- lines << Locale.t("slots.template", name: template) if template && !template.to_s.empty?
16
16
  calendars.each do |cal|
17
- lines << cal[:name].to_s
17
+ lines << "- #{cal[:name]}"
18
18
  end
19
- lines << google_calendar_day_url(from)
19
+ lines << "link: #{google_calendar_day_url(from)}"
20
20
  lines.join("\n")
21
21
  end
22
22
 
@@ -25,7 +25,7 @@ module FlycalCli
25
25
 
26
26
  slots_by_day.sort_by(&:first).each do |date, slots|
27
27
  lines << "" unless lines.empty?
28
- lines << day_header(date)
28
+ lines << underline(day_header(date))
29
29
  slots.each do |start_at, end_at|
30
30
  lines << slot_range(start_at, end_at)
31
31
  end
@@ -34,6 +34,10 @@ module FlycalCli
34
34
  lines.join("\n")
35
35
  end
36
36
 
37
+ def strip_ansi(text)
38
+ text.to_s.gsub(/\e\[[0-9;]*m/, "")
39
+ end
40
+
37
41
  private
38
42
 
39
43
  def underline(str)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FlycalCli
4
- VERSION = '0.7.0'
4
+ VERSION = '0.7.2'
5
5
  end
data/locales/en.json CHANGED
@@ -31,8 +31,7 @@
31
31
  "slots": {
32
32
  "no_available": "No available slots found.",
33
33
  "no_calendar": "No calendar found. Run 'flycal config' to set a default.",
34
- "header": "found %{count} slots\nfrom %{from} to %{to}\nwith duration %{duration}\nfor calendars",
35
- "template": "template %{name}",
34
+ "header": "found %{count} slots\nfrom %{from} to %{to}\nwith duration %{duration}, template %{template}\nconsidering calendars",
36
35
  "copied_to_clipboard": "✓ Slot list copied to clipboard (pbcopy)"
37
36
  },
38
37
  "errors": {
data/locales/it.json CHANGED
@@ -31,8 +31,7 @@
31
31
  "slots": {
32
32
  "no_available": "Nessuno slot disponibile.",
33
33
  "no_calendar": "Nessun calendario trovato. Esegui 'flycal config' per impostare il predefinito.",
34
- "header": "trovati %{count} slot\nda %{from} a %{to}\ncon durata %{duration}\nper i calendari",
35
- "template": "template %{name}",
34
+ "header": "trovati %{count} slot\nda %{from} a %{to}\ncon durata %{duration}, template %{template}\nconsiderando i calendari",
36
35
  "copied_to_clipboard": "✓ Lista slot copiata negli appunti (pbcopy)"
37
36
  },
38
37
  "errors": {
data/mcp/tools.json CHANGED
@@ -274,7 +274,7 @@
274
274
  },
275
275
  "output": {
276
276
  "format": "text/plain",
277
- "example": "found 3 slots\nfrom wed 29 July 2026 to wed 5 August 2026\nwith duration 45min\nfor calendars\ntemplate work\nWork\nhttps://calendar.google.com/calendar/r/day/2026/7/29\n\nfriday 15/7\n10 - 12\n13 - 15.30"
277
+ "example": "found 6 slots\nfrom sat 1 August 2026 to sat 8 August 2026\nwith duration 45min, template dinner\nconsidering calendars\n- Work\nlink: https://calendar.google.com/calendar/r/day/2026/8/1\n\nsaturday 1/8\n19 - 23"
278
278
  },
279
279
  "inputSchema": {
280
280
  "type": "object",
@@ -287,7 +287,7 @@
287
287
  },
288
288
  "from": {
289
289
  "type": "string",
290
- "description": "Start date/time. Default: now. YYYY-MM-DD always; with locale it also DD-MM-YYYY; with locale en also MM-DD-YYYY. Separators - or /. Optional time."
290
+ "description": "Start date/time. Default: now. Absolute dates, or relative: monday, next monday, tomorrow, lunedi, prossimo lunedi, etc."
291
291
  },
292
292
  "in": {
293
293
  "type": "string",
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.0
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - flycal-cli