calendar-assistant 0.2.1 → 0.3.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.
data/Rakefile CHANGED
@@ -1,9 +1,17 @@
1
1
  #require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
3
  require "concourse"
4
+ require "license_finder"
4
5
 
5
6
  Concourse.new("calendar-assistant").create_tasks!
6
7
 
7
8
  RSpec::Core::RakeTask.new(:spec)
8
9
 
9
- task :default => :spec
10
+ desc "Ensure all dependencies meet license requirements."
11
+ task :license_finder do
12
+ LicenseFinder::CLI::Main.start(["report"])
13
+ LicenseFinder::CLI::Main.start([])
14
+ end
15
+
16
+ desc "Run specs and license finder"
17
+ task :default => [:spec, :license_finder]
@@ -1,192 +1,40 @@
1
- # coding: utf-8
2
- require "google/apis/calendar_v3"
3
- require "json"
4
- require "yaml"
5
- require "business_time"
6
- require "rainbow"
7
- require "set"
8
-
9
- require "calendar_assistant/version"
1
+ #
2
+ # stdlib
3
+ #
4
+ autoload :YAML, "yaml"
5
+ autoload :URI, "uri"
6
+ autoload :Set, "set"
7
+ autoload :FileUtils, "fileutils"
8
+
9
+ #
10
+ # gem dependencies
11
+ #
12
+ autoload :BusinessTime, "business_time"
13
+ autoload :Chronic, "chronic"
14
+ autoload :ChronicDuration, "chronic_duration"
15
+ autoload :Google, "calendar_assistant/extensions/google_apis_extensions"
16
+ autoload :Launchy, "launchy"
17
+ autoload :TOML, "toml"
18
+ autoload :Thor, "thor"
19
+ require "calendar_assistant/extensions/rainbow_extensions" # Rainbow() doesn't trigger autoload
20
+
21
+ #
22
+ # CalendarAssistant and associated classes
23
+ #
24
+ require "calendar_assistant/calendar_assistant"
10
25
 
11
26
  class CalendarAssistant
12
- GCal = Google::Apis::CalendarV3
13
-
14
- class BaseException < RuntimeError ; end
15
-
16
- EMOJI_WORLDMAP = "🗺" # U+1F5FA WORLD MAP
17
- EMOJI_PLANE = "🛪" # U+1F6EA NORTHEAST-POINTING AIRPLANE
18
- EMOJI_1_1 = "👫" # MAN AND WOMAN HOLDING HANDS
19
-
20
- DEFAULT_CALENDAR_ID = "primary"
21
-
22
- attr_reader :service, :calendar, :config
23
-
24
- def self.authorize profile_name
25
- config = CalendarAssistant::Config.new
26
- Authorizer.new(profile_name, config.token_store).authorize
27
- end
28
-
29
- def self.date_range_cast time_range
30
- time_range.first.to_date..(time_range.last + 1.day).to_date
31
- end
32
-
33
- def initialize config=CalendarAssistant::Config.new, event_repository: nil
34
- @config = config
35
- @service = Authorizer.new(config.profile_name, config.token_store).service
36
- @calendar = service.get_calendar DEFAULT_CALENDAR_ID
37
- @event_repository = event_repository || EventRepository.new(@service, DEFAULT_CALENDAR_ID)
38
- end
39
-
40
- def find_events time_range
41
- @event_repository.find(time_range)
42
- end
43
-
44
- def availability time_range
45
- length = ChronicDuration.parse(config.setting(Config::Keys::Settings::MEETING_LENGTH))
46
-
47
- start_of_day = Chronic.parse(config.setting(Config::Keys::Settings::START_OF_DAY))
48
- start_of_day = start_of_day - start_of_day.beginning_of_day
49
-
50
- end_of_day = Chronic.parse(config.setting(Config::Keys::Settings::END_OF_DAY))
51
- end_of_day = end_of_day - end_of_day.beginning_of_day
52
-
53
- events = find_events time_range
54
- date_range = time_range.first.to_date .. time_range.last.to_date
55
-
56
- # find relevant events and map them into dates
57
- dates_events = date_range.inject({}) { |de, date| de[date] = [] ; de }
58
- events.each do |event|
59
- if event.accepted?
60
- event_date = event.start.to_date!
61
- dates_events[event_date] ||= []
62
- dates_events[event_date] << event
63
- end
64
- dates_events
65
- end
66
-
67
- # iterate over the days finding free chunks of time
68
- avail_time = date_range.inject({}) do |avail_time, date|
69
- avail_time[date] ||= []
70
- date_events = dates_events[date]
71
-
72
- start_time = date.to_time + start_of_day
73
- end_time = date.to_time + end_of_day
74
-
75
- date_events.each do |e|
76
- if (e.start.date_time.to_time - start_time) >= length
77
- avail_time[date] << CalendarAssistant.available_block(start_time.to_datetime, e.start.date_time)
78
- end
79
- start_time = e.end.date_time.to_time
80
- break if start_time >= end_time
81
- end
82
-
83
- if end_time - start_time >= length
84
- avail_time[date] << CalendarAssistant.available_block(start_time.to_datetime, end_time.to_datetime)
85
- end
86
-
87
- avail_time
88
- end
89
-
90
- avail_time
91
- end
92
-
93
- def find_location_events time_range
94
- @event_repository.find(time_range).select { |e| e.location_event? }
95
- end
96
-
97
- def create_location_event time_range, location
98
- # find pre-existing events that overlap
99
- existing_events = find_location_events time_range
100
-
101
- # augment event end date appropriately
102
- range = CalendarAssistant.date_range_cast time_range
103
-
104
- deleted_events = []
105
- modified_events = []
106
-
107
- event = @event_repository.create(transparency: GCal::Event::Transparency::TRANSPARENT, start: range.first, end: range.last , summary: "#{EMOJI_WORLDMAP} #{location}")
108
-
109
- existing_events.each do |existing_event|
110
- if existing_event.start.date >= event.start.date && existing_event.end.date <= event.end.date
111
- @event_repository.delete existing_event
112
- deleted_events << existing_event
113
- elsif existing_event.start.date <= event.end.date && existing_event.end.date > event.end.date
114
- @event_repository.update existing_event, start: range.last
115
- modified_events << existing_event
116
- elsif existing_event.start.date < event.start.date && existing_event.end.date >= event.start.date
117
- @event_repository.update existing_event, end: range.first
118
- modified_events << existing_event
119
- end
120
- end
121
-
122
- response = {created: [event]}
123
- response[:deleted] = deleted_events unless deleted_events.empty?
124
- response[:modified] = modified_events unless modified_events.empty?
125
- response
126
- end
127
-
128
- def event_description event
129
- s = sprintf("%-25.25s", event_date_description(event))
130
-
131
- date_ansi_codes = []
132
- date_ansi_codes << :bright if event.current?
133
- date_ansi_codes << :faint if event.past?
134
- s = date_ansi_codes.inject(Rainbow(s)) { |text, ansi| text.send ansi }
135
-
136
- s += Rainbow(sprintf(" | %s", event.view_summary)).bold
137
-
138
- attributes = []
139
- unless event.private?
140
- attributes << "recurring" if event.recurring_event_id
141
- attributes << "not-busy" unless event.busy?
142
- attributes << "self" if event.human_attendees.nil? && event.visibility != "private"
143
- attributes << "1:1" if event.one_on_one?
144
- end
145
-
146
- attributes << event.visibility if event.explicit_visibility?
147
-
148
- s += Rainbow(sprintf(" (%s)", attributes.to_a.sort.join(", "))).italic unless attributes.empty?
149
-
150
- s = Rainbow(Rainbow.uncolor(s)).faint.strike if event.declined?
151
-
152
- s
153
- end
154
-
155
- def event_date_description event
156
- if event.all_day?
157
- start_date = event.start.to_date
158
- end_date = event.end.to_date
159
- if (end_date - start_date) <= 1
160
- event.start.to_s
161
- else
162
- sprintf("%s - %s", start_date, end_date - 1.day)
163
- end
164
- else
165
- if event.start.date_time.to_date == event.end.date_time.to_date
166
- sprintf("%s - %s", event.start.date_time.strftime("%Y-%m-%d %H:%M"), event.end.date_time.strftime("%H:%M"))
167
- else
168
- sprintf("%s - %s", event.start.date_time.strftime("%Y-%m-%d %H:%M"), event.end.date_time.strftime("%Y-%m-%d %H:%M"))
169
- end
170
- end
171
- end
172
-
173
- private
174
-
175
- def self.available_block start_time, end_time
176
- Google::Apis::CalendarV3::Event.new(
177
- start: Google::Apis::CalendarV3::EventDateTime.new(date_time: start_time),
178
- end: Google::Apis::CalendarV3::EventDateTime.new(date_time: end_time),
179
- summary: "available"
180
- )
181
- end
27
+ autoload :VERSION, "calendar_assistant/version"
28
+ autoload :Config, "calendar_assistant/config"
29
+ autoload :Authorizer, "calendar_assistant/authorizer"
30
+ autoload :StringHelpers, "calendar_assistant/string_helpers"
31
+ autoload :DateHelpers, "calendar_assistant/date_helpers"
32
+ autoload :Event, "calendar_assistant/event"
33
+ autoload :EventRepository, "calendar_assistant/event_repository"
34
+ autoload :EventRepositoryFactory, "calendar_assistant/event_repository_factory"
35
+ autoload :EventSet, "calendar_assistant/event_set"
36
+ autoload :Scheduler, "calendar_assistant/scheduler"
37
+ autoload :LocalService, "calendar_assistant/local_service"
182
38
  end
183
39
 
184
- require "calendar_assistant/config"
185
- require "calendar_assistant/authorizer"
186
40
  require "calendar_assistant/cli"
187
- require "calendar_assistant/string_helpers"
188
- require "calendar_assistant/extensions/event_date_time_extensions"
189
- require "calendar_assistant/extensions/event_extensions"
190
- require "calendar_assistant/event"
191
- require "calendar_assistant/event_repository"
192
- require "calendar_assistant/extensions/rainbow_extensions"
@@ -17,9 +17,6 @@
17
17
  # See the License for the specific language governing permissions and
18
18
  # limitations under the License.
19
19
 
20
- require 'googleauth'
21
- require 'rainbow'
22
-
23
20
  class CalendarAssistant
24
21
  class Authorizer
25
22
  class NoCredentials < CalendarAssistant::BaseException ; end
@@ -0,0 +1,122 @@
1
+ # coding: utf-8
2
+ class CalendarAssistant
3
+ class BaseException < RuntimeError ; end
4
+
5
+ EMOJI_WORLDMAP = "🗺" # U+1F5FA WORLD MAP
6
+ EMOJI_PLANE = "🛪" # U+1F6EA NORTHEAST-POINTING AIRPLANE
7
+ EMOJI_1_1 = "👫" # MAN AND WOMAN HOLDING HANDS
8
+
9
+ attr_reader :service, :calendar, :config
10
+
11
+ def self.authorize profile_name
12
+ config = CalendarAssistant::Config.new
13
+ Authorizer.new(profile_name, config.token_store).authorize
14
+ end
15
+
16
+ def self.date_range_cast time_range
17
+ time_range.first.to_date..(time_range.last + 1.day).to_date
18
+ end
19
+
20
+ def self.in_tz time_zone, &block
21
+ # this is totally not thread-safe
22
+ orig_time_tz = Time.zone
23
+ orig_env_tz = ENV['TZ']
24
+ begin
25
+ unless time_zone.nil?
26
+ Time.zone = time_zone
27
+ ENV['TZ'] = time_zone
28
+ end
29
+ yield
30
+ ensure
31
+ Time.zone = orig_time_tz
32
+ ENV['TZ'] = orig_env_tz
33
+ end
34
+ end
35
+
36
+
37
+ def initialize config=Config.new,
38
+ event_repository_factory: EventRepositoryFactory
39
+ @config = config
40
+
41
+ if filename = config.setting(Config::Keys::Options::LOCAL_STORE)
42
+ @service = CalendarAssistant::LocalService.new(file: filename)
43
+ else
44
+ @service = Authorizer.new(config.profile_name, config.token_store).service
45
+ end
46
+ @calendar = service.get_calendar Config::DEFAULT_CALENDAR_ID
47
+ @event_repository_factory = event_repository_factory
48
+ @event_repositories = {} # calendar_id → event_repository
49
+ end
50
+
51
+ def in_env &block
52
+ # this is totally not thread-safe
53
+ config.in_env do
54
+ in_tz do
55
+ yield
56
+ end
57
+ end
58
+ end
59
+
60
+ def in_tz &block
61
+ CalendarAssistant.in_tz calendar.time_zone do
62
+ yield
63
+ end
64
+ end
65
+
66
+ def find_events time_range
67
+ calendar_ids = config.attendees
68
+ if calendar_ids.length > 1
69
+ raise "CalendarAssistant#find_events only supports one person (for now)"
70
+ end
71
+ event_repository(calendar_ids.first).find(time_range)
72
+ end
73
+
74
+ def availability time_range
75
+ calendar_ids = config.attendees
76
+ ers = calendar_ids.map do |calendar_id|
77
+ event_repository calendar_id
78
+ end
79
+ Scheduler.new(self, ers).available_blocks(time_range)
80
+ end
81
+
82
+ def find_location_events time_range
83
+ event_set = event_repository.find(time_range)
84
+ event_set.new event_set.events.select { |e| e.location_event? }
85
+ end
86
+
87
+ def create_location_event time_range, location
88
+ # find pre-existing events that overlap
89
+ existing_event_set = find_location_events time_range
90
+
91
+ # augment event end date appropriately
92
+ range = CalendarAssistant.date_range_cast time_range
93
+
94
+ deleted_events = []
95
+ modified_events = []
96
+
97
+ event = event_repository.create(transparency: CalendarAssistant::Event::Transparency::TRANSPARENT, start: range.first, end: range.last , summary: "#{EMOJI_WORLDMAP} #{location}")
98
+
99
+ existing_event_set.events.each do |existing_event|
100
+ if existing_event.start_date >= event.start_date && existing_event.end_date <= event.end_date
101
+ event_repository.delete existing_event
102
+ deleted_events << existing_event
103
+ elsif existing_event.start_date <= event.end_date && existing_event.end_date > event.end_date
104
+ event_repository.update existing_event, start: range.last
105
+ modified_events << existing_event
106
+ elsif existing_event.start_date < event.start_date && existing_event.end_date >= event.start_date
107
+ event_repository.update existing_event, end: range.first
108
+ modified_events << existing_event
109
+ end
110
+ end
111
+
112
+ response = {created: [event]}
113
+ response[:deleted] = deleted_events unless deleted_events.empty?
114
+ response[:modified] = modified_events unless modified_events.empty?
115
+
116
+ existing_event_set.new response
117
+ end
118
+
119
+ def event_repository calendar_id=Config::DEFAULT_CALENDAR_ID
120
+ @event_repositories[calendar_id] ||= @event_repository_factory.new_event_repository(@service, calendar_id)
121
+ end
122
+ end
@@ -1,17 +1,25 @@
1
- require "thor"
2
- require "chronic"
3
- require "chronic_duration"
4
- require "launchy"
5
-
6
1
  require "calendar_assistant/cli_helpers"
7
2
 
8
3
  class CalendarAssistant
9
4
  class CLI < Thor
10
- def self.supports_profile_option
11
- option :profile,
5
+ def self.will_create_a_service
6
+ option CalendarAssistant::Config::Keys::Settings::PROFILE,
12
7
  type: :string,
13
8
  desc: "the profile you'd like to use (if different from default)",
14
9
  aliases: ["-p"]
10
+
11
+ option CalendarAssistant::Config::Keys::Options::LOCAL_STORE,
12
+ type: :string,
13
+ banner: "FILENAME",
14
+ desc: "Load events from a local file instead of Google Calendar"
15
+ end
16
+
17
+ def self.has_attendees
18
+ option CalendarAssistant::Config::Keys::Options::ATTENDEES,
19
+ type: :string,
20
+ banner: "ATTENDEE1[,ATTENDEE2[,...]]",
21
+ desc: "[default 'me'] people (email IDs) to whom this command will be applied",
22
+ aliases: ["-a"]
15
23
  end
16
24
 
17
25
  default_config = CalendarAssistant::Config.new options: options # used in option descriptions
@@ -19,22 +27,25 @@ class CalendarAssistant
19
27
  class_option :help,
20
28
  type: :boolean,
21
29
  aliases: ["-h", "-?"]
22
- class_option :debug,
30
+ class_option CalendarAssistant::Config::Keys::Options::DEBUG,
23
31
  type: :boolean,
24
32
  desc: "how dare you suggest there are bugs"
25
33
 
26
34
 
35
+ desc "version",
36
+ "Display the version of calendar-assistant"
37
+ def version
38
+ return if handle_help_args
39
+ out.puts CalendarAssistant::VERSION
40
+ end
41
+
42
+
27
43
  desc "config",
28
44
  "Dump your configuration parameters (merge of defaults and overrides from #{CalendarAssistant::Config::CONFIG_FILE_PATH})"
29
45
  def config
30
46
  return if handle_help_args
31
- config = CalendarAssistant::Config.new
32
- settings = {}
33
- setting_names = CalendarAssistant::Config::Keys::Settings.constants.map { |k| CalendarAssistant::Config::Keys::Settings.const_get k }
34
- setting_names.each do |key|
35
- settings[key] = config.setting key
36
- end
37
- puts TOML::Generator.new({CalendarAssistant::Config::Keys::SETTINGS => settings}).body
47
+ settings = CalendarAssistant::Config.new.settings
48
+ out.puts TOML::Generator.new({CalendarAssistant::Config::Keys::SETTINGS => settings}).body
38
49
  end
39
50
 
40
51
 
@@ -50,11 +61,12 @@ class CalendarAssistant
50
61
  file to `#{CalendarAssistant::Authorizer::CREDENTIALS_PATH}`
51
62
  EOD
52
63
  def setup
53
- out = CLIHelpers::Out.new
64
+ # TODO ugh see #34 for advice on how to clean this up
65
+ return if handle_help_args
54
66
  if File.exist? CalendarAssistant::Authorizer::CREDENTIALS_PATH
55
67
  out.puts sprintf("Credentials already exist in %s",
56
68
  CalendarAssistant::Authorizer::CREDENTIALS_PATH)
57
- exit 0
69
+ return
58
70
  end
59
71
 
60
72
  out.launch "https://developers.google.com/calendar/quickstart/ruby"
@@ -92,8 +104,10 @@ class CalendarAssistant
92
104
  In order for this to work, you'll need to have set up your API client
93
105
  credentials. Run `calendar-assistant help setup` for instructions.
94
106
  EOD
95
- def authorize profile_name
107
+ def authorize profile_name=nil
96
108
  return if handle_help_args
109
+ return help! if profile_name.nil?
110
+
97
111
  CalendarAssistant.authorize profile_name
98
112
  puts "\nYou're authorized!\n\n"
99
113
  end
@@ -101,64 +115,70 @@ class CalendarAssistant
101
115
 
102
116
  desc "show [DATE | DATERANGE | TIMERANGE]",
103
117
  "Show your events for a date or range of dates (default 'today')"
104
- option :commitments,
118
+ option CalendarAssistant::Config::Keys::Options::COMMITMENTS,
105
119
  type: :boolean,
106
120
  desc: "only show events that you've accepted with another person",
107
121
  aliases: ["-c"]
108
- supports_profile_option
122
+ will_create_a_service
123
+ has_attendees
109
124
  def show datespec="today"
110
125
  return if handle_help_args
111
- config = CalendarAssistant::Config.new options: options
126
+ config = CalendarAssistant::Config.new(options: options)
112
127
  ca = CalendarAssistant.new config
113
- events = ca.find_events CLIHelpers.parse_datespec(datespec)
114
- CLIHelpers::Out.new.print_events ca, events, options
128
+ ca.in_env do
129
+ event_set = ca.find_events CLIHelpers.parse_datespec(datespec)
130
+ out.print_events ca, event_set
131
+ end
115
132
  end
116
133
 
117
134
 
118
135
  desc "join [TIME]",
119
136
  "Open the URL for a video call attached to your meeting at time TIME (default 'now')"
120
- option :join,
137
+ option CalendarAssistant::Config::Keys::Options::JOIN,
121
138
  type: :boolean, default: true,
122
139
  desc: "launch a browser to join the video call URL"
123
- supports_profile_option
140
+ will_create_a_service
124
141
  def join timespec="now"
125
142
  return if handle_help_args
126
- config = CalendarAssistant::Config.new options: options
127
- ca = CalendarAssistant.new config
128
- event, url = CLIHelpers.find_av_uri ca, timespec
129
- if event
130
- CLIHelpers::Out.new.print_events ca, event, options
131
- CLIHelpers::Out.new.puts url
132
- if options[:join]
133
- CLIHelpers::Out.new.launch url
143
+ ca = CalendarAssistant.new CalendarAssistant::Config.new(options: options)
144
+ ca.in_env do
145
+ event_set, url = CLIHelpers.find_av_uri ca, timespec
146
+ if ! event_set.empty?
147
+ out.print_events ca, event_set
148
+ out.puts url
149
+ out.launch url if options[CalendarAssistant::Config::Keys::Options::JOIN]
150
+ else
151
+ out.puts "Could not find a meeting '#{timespec}' with a video call to join."
134
152
  end
135
- else
136
- CLIHelpers::Out.new.puts "Could not find a meeting '#{timespec}' with a video call to join."
137
153
  end
138
154
  end
139
155
 
140
156
 
141
157
  desc "location [DATE | DATERANGE]",
142
158
  "Show your location for a date or range of dates (default 'today')"
143
- supports_profile_option
159
+ will_create_a_service
144
160
  def location datespec="today"
145
161
  return if handle_help_args
146
- config = CalendarAssistant::Config.new options: options
147
- ca = CalendarAssistant.new config
148
- events = ca.find_location_events CLIHelpers.parse_datespec(datespec)
149
- CLIHelpers::Out.new.print_events ca, events, options
162
+ ca = CalendarAssistant.new CalendarAssistant::Config.new(options: options)
163
+ ca.in_env do
164
+ event_set = ca.find_location_events CLIHelpers.parse_datespec(datespec)
165
+ out.print_events ca, event_set
166
+ end
150
167
  end
151
168
 
152
169
 
153
170
  desc "location-set LOCATION [DATE | DATERANGE]",
154
171
  "Set your location to LOCATION for a date or range of dates (default 'today')"
155
- supports_profile_option
156
- def location_set location, datespec="today"
172
+ will_create_a_service
173
+ def location_set location=nil, datespec="today"
157
174
  return if handle_help_args
158
- config = CalendarAssistant::Config.new options: options
159
- ca = CalendarAssistant.new config
160
- events = ca.create_location_event CLIHelpers.parse_datespec(datespec), location
161
- CLIHelpers::Out.new.print_events ca, events, options
175
+ return help! if location.nil?
176
+
177
+ ca = CalendarAssistant.new CalendarAssistant::Config.new(options: options)
178
+ ca.in_env do
179
+ event_set = ca.create_location_event CLIHelpers.parse_datespec(datespec), location
180
+ out.print_events ca, event_set
181
+ end
162
182
  end
163
183
 
164
184
 
@@ -173,29 +193,39 @@ class CalendarAssistant
173
193
  option CalendarAssistant::Config::Keys::Settings::START_OF_DAY,
174
194
  type: :string,
175
195
  banner: "TIME",
176
- desc: sprintf("[default %s] find chunks of available time after TIME (which is a Chronic string like '9am' or '14:30')",
196
+ desc: sprintf("[default %s] find chunks of available time after TIME (which is a BusinessTime string like '9am' or '14:30')",
177
197
  default_config.setting(CalendarAssistant::Config::Keys::Settings::START_OF_DAY)),
178
198
  aliases: ["-s"]
179
199
  option CalendarAssistant::Config::Keys::Settings::END_OF_DAY,
180
200
  type: :string,
181
201
  banner: "TIME",
182
- desc: sprintf("[default %s] find chunks of available time before TIME (which is a Chronic string like '9am' or '14:30')",
202
+ desc: sprintf("[default %s] find chunks of available time before TIME (which is a BusinessTime string like '9am' or '14:30')",
183
203
  default_config.setting(CalendarAssistant::Config::Keys::Settings::END_OF_DAY)),
184
204
  aliases: ["-e"]
185
- supports_profile_option
205
+ has_attendees
206
+ will_create_a_service
186
207
  def availability datespec="today"
187
208
  return if handle_help_args
188
- config = CalendarAssistant::Config.new options: options
189
- ca = CalendarAssistant.new config
190
- events = ca.availability CLIHelpers.parse_datespec(datespec)
191
- CLIHelpers::Out.new.print_available_blocks ca, events, options
209
+ ca = CalendarAssistant.new CalendarAssistant::Config.new(options: options)
210
+ ca.in_env do
211
+ event_set = ca.availability CLIHelpers.parse_datespec(datespec)
212
+ out.print_available_blocks ca, event_set
213
+ end
192
214
  end
193
215
 
194
216
  private
195
217
 
218
+ def out
219
+ @out ||= CLIHelpers::Out.new
220
+ end
221
+
222
+ def help!
223
+ help(current_command_chain.first)
224
+ end
225
+
196
226
  def handle_help_args
197
227
  if options[:help]
198
- help(current_command_chain.first)
228
+ help!
199
229
  return true
200
230
  end
201
231
  end