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 +4 -4
- data/README.md +20 -16
- data/lib/flycal_cli/cli.rb +2 -2
- data/lib/flycal_cli/date_time_parser.rb +95 -7
- data/lib/flycal_cli/slot_formatter.rb +10 -6
- data/lib/flycal_cli/version.rb +1 -1
- data/locales/en.json +1 -2
- data/locales/it.json +1 -2
- data/mcp/tools.json +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 371b44577d9ce6befe5b01bc54768999844615943b982a6e9cd9cd8c9737d007
|
|
4
|
+
data.tar.gz: e3c6c00e03e38eccaea97ca045582a35d737916f1203963cc2c989ebf75de46b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`).
|
|
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
|
|
178
|
-
from
|
|
179
|
-
with duration 45min
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
https://calendar.google.com/calendar/r/day/2026/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
data/lib/flycal_cli/cli.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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]
|
|
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)
|
data/lib/flycal_cli/version.rb
CHANGED
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}\
|
|
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}\
|
|
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
|
|
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.
|
|
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",
|