dsu 0.1.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.reek.yml +20 -0
- data/.rspec +3 -0
- data/.rubocop.yml +192 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +2 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +21 -0
- data/README.md +128 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/dsu +11 -0
- data/lib/dsu/cli.rb +178 -0
- data/lib/dsu/command_services/add_entry_service.rb +61 -0
- data/lib/dsu/models/entry.rb +49 -0
- data/lib/dsu/models/entry_group.rb +70 -0
- data/lib/dsu/services/configuration_loader_service.rb +34 -0
- data/lib/dsu/services/entry_group_deleter_service.rb +31 -0
- data/lib/dsu/services/entry_group_hydrator_service.rb +43 -0
- data/lib/dsu/services/entry_group_reader_service.rb +36 -0
- data/lib/dsu/services/entry_group_writer_service.rb +45 -0
- data/lib/dsu/services/entry_hydrator_service.rb +35 -0
- data/lib/dsu/subcommands/config.rb +49 -0
- data/lib/dsu/support/ask.rb +38 -0
- data/lib/dsu/support/colorable.rb +13 -0
- data/lib/dsu/support/commander/command.rb +130 -0
- data/lib/dsu/support/commander/command_help.rb +62 -0
- data/lib/dsu/support/commander/subcommand.rb +45 -0
- data/lib/dsu/support/configuration.rb +89 -0
- data/lib/dsu/support/entry_group_fileable.rb +41 -0
- data/lib/dsu/support/entry_group_loadable.rb +52 -0
- data/lib/dsu/support/field_errors.rb +11 -0
- data/lib/dsu/support/folder_locations.rb +21 -0
- data/lib/dsu/support/interactive/cli.rb +161 -0
- data/lib/dsu/support/say.rb +40 -0
- data/lib/dsu/support/time_formatable.rb +42 -0
- data/lib/dsu/validators/entries_validator.rb +64 -0
- data/lib/dsu/validators/time_validator.rb +34 -0
- data/lib/dsu/version.rb +5 -0
- data/lib/dsu/views/entry_group/show.rb +60 -0
- data/lib/dsu.rb +38 -0
- data/sig/dsu.rbs +4 -0
- metadata +199 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'command_help'
|
4
|
+
require_relative 'subcommand'
|
5
|
+
|
6
|
+
module Dsu
|
7
|
+
module Support
|
8
|
+
module Commander
|
9
|
+
# https://www.toptal.com/ruby/ruby-dsl-metaprogramming-guide
|
10
|
+
module Command
|
11
|
+
class << self
|
12
|
+
def included(base)
|
13
|
+
base.extend ClassMethods
|
14
|
+
base.engine.command_namespace to_command_namespace_symbol base.name
|
15
|
+
base.engine.command_prompt base.engine.command_namespace
|
16
|
+
binding.pry
|
17
|
+
base.singleton_class.delegate :command_namespace, :command_namespaces, :commands,
|
18
|
+
:command_add, :command_subcommand_add, :command_prompt, :command_parent,
|
19
|
+
:help, to: :engine
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def to_command_namespace_symbol(namespace, join_token: '_')
|
25
|
+
namespace.delete(':').split(/(?=[A-Z])/).join(join_token).downcase
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def command_subcommand_create(command_parent:)
|
31
|
+
new.tap do |subcommand|
|
32
|
+
subcommand.extend Subcommand
|
33
|
+
subcommand.command_parent command_parent
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def engine
|
38
|
+
@engine ||= Engine.new(owning_command: self)
|
39
|
+
end
|
40
|
+
|
41
|
+
class Engine
|
42
|
+
include CommandHelp
|
43
|
+
|
44
|
+
attr_reader :owning_command
|
45
|
+
|
46
|
+
def initialize(owning_command:)
|
47
|
+
@owning_command = owning_command
|
48
|
+
end
|
49
|
+
|
50
|
+
def command_add(command:, desc:, long_desc: nil, options: {}, commands: [])
|
51
|
+
self.commands[command_namespace] ||= {}
|
52
|
+
self.commands[command_namespace][command] = {
|
53
|
+
desc: desc,
|
54
|
+
long_desc: long_desc,
|
55
|
+
options: options,
|
56
|
+
commands: commands,
|
57
|
+
help: command_help_for(command: command, desc: desc,
|
58
|
+
long_desc: long_desc, options: options, commands: commands)
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def command_subcommand_add(subcommand, command_parent: nil)
|
63
|
+
command_parent ||= @owning_command
|
64
|
+
subcommand = subcommand.command_subcommand_create command_parent: command_parent
|
65
|
+
commands[command_namespace] ||= {}
|
66
|
+
binding.pry
|
67
|
+
subcommand.command_namespaces.each_with_index do |namespace, index|
|
68
|
+
next if index.zero?
|
69
|
+
|
70
|
+
target = commands.dig(*subcommand.command_namespaces[1..index])
|
71
|
+
target ||= commands.dig(*subcommand.command_namespaces[0..index - 1])
|
72
|
+
target[namespace] ||= {}
|
73
|
+
end
|
74
|
+
commands.dig(*subcommand.command_namespaces[0..])[subcommand.command_namespaces.last] = subcommand
|
75
|
+
|
76
|
+
# subcommand.commands.each do |command_namespace, command|
|
77
|
+
# command.each do |subcommand_command, data|
|
78
|
+
# commands[self.command_namespace][command_namespace] ||= {}
|
79
|
+
# commands[self.command_namespace][command_namespace][subcommand_command] = {
|
80
|
+
# desc: data[:desc],
|
81
|
+
# long_desc: data[:long_desc],
|
82
|
+
# options: data[:options],
|
83
|
+
# commands: data[:commands],
|
84
|
+
# help: command_help_for(command: subcommand_command, namespaces: subcommand.command_namespaces, desc: data[:desc],
|
85
|
+
# long_desc: data[:long_desc], options: data[:options], commands: data[:commands])
|
86
|
+
# }
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
end
|
90
|
+
|
91
|
+
def command_namespaces(namespaces = [])
|
92
|
+
command_parent&.command_namespaces(namespaces)
|
93
|
+
|
94
|
+
namespaces << command_namespace
|
95
|
+
namespaces
|
96
|
+
end
|
97
|
+
|
98
|
+
def command_namespace(namespace = nil)
|
99
|
+
return @command_namespace || name if namespace.nil?
|
100
|
+
|
101
|
+
@command_namespace = namespace
|
102
|
+
end
|
103
|
+
|
104
|
+
def command_prompt(value = nil)
|
105
|
+
return @command_prompt || name if value.nil?
|
106
|
+
|
107
|
+
@command_prompt = value
|
108
|
+
end
|
109
|
+
|
110
|
+
def command_parent(parent = nil)
|
111
|
+
return @command_parent if parent.nil?
|
112
|
+
|
113
|
+
@command_parent = parent
|
114
|
+
end
|
115
|
+
|
116
|
+
def commands
|
117
|
+
@commands ||= {}
|
118
|
+
end
|
119
|
+
|
120
|
+
def help
|
121
|
+
commands.each do |_command, command_data|
|
122
|
+
puts "#{command_namespaces.join(' ')} #{command_data[:help]}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
4
|
+
module Support
|
5
|
+
module Commander
|
6
|
+
module CommandHelp
|
7
|
+
private
|
8
|
+
|
9
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
10
|
+
def command_help_for(command:, desc:, namespaces: nil, long_desc: nil, options: {}, commands: [])
|
11
|
+
namespaces ||= command_namespaces
|
12
|
+
help =
|
13
|
+
<<~HELP
|
14
|
+
#{namespaces&.join(' ')} #{command}#{' [OPTIONS]' if options&.any?} - #{desc}
|
15
|
+
#{'OPTIONS:' if options&.any?}
|
16
|
+
#{options_help_for options}
|
17
|
+
#{'OPTION ALIASES:' if any_option_aliases_for?(options)}
|
18
|
+
#{options_aliases_help_for options}
|
19
|
+
#{'---' unless long_desc.blank?}
|
20
|
+
#{long_desc}
|
21
|
+
HELP
|
22
|
+
help.gsub(/\n{2,}/, "\n")
|
23
|
+
end
|
24
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
25
|
+
|
26
|
+
def options_help_for(options)
|
27
|
+
return [] if options.blank?
|
28
|
+
|
29
|
+
options.map do |option, data|
|
30
|
+
type = option_to_a(data[:type])&.join(' | ')
|
31
|
+
type = :boolean if type.blank?
|
32
|
+
"#{option} <#{type}>, default: #{data[:default]}"
|
33
|
+
end.join("\n")
|
34
|
+
end
|
35
|
+
|
36
|
+
def options_aliases_help_for(options)
|
37
|
+
return unless any_option_aliases_for?(options)
|
38
|
+
|
39
|
+
options.filter_map do |option, data|
|
40
|
+
aliases = option_to_a(data[:aliases])&.join(' | ')
|
41
|
+
<<~HELP
|
42
|
+
#{option} aliases: [#{aliases}]
|
43
|
+
HELP
|
44
|
+
end.join("\n")
|
45
|
+
end
|
46
|
+
|
47
|
+
def any_option_aliases_for?(options)
|
48
|
+
return false if options.blank?
|
49
|
+
|
50
|
+
options.keys.any? { |key| options.dig(key, :aliases).any? }
|
51
|
+
end
|
52
|
+
|
53
|
+
def option_to_a(option)
|
54
|
+
return [] if option.blank?
|
55
|
+
return option if option.is_a? Array
|
56
|
+
|
57
|
+
[option]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
4
|
+
module Support
|
5
|
+
module Commander
|
6
|
+
# Subcommands should extend this module once they are instantiated
|
7
|
+
# so that the Subcommand instance has access to all necessary
|
8
|
+
# class methods for this subcommand to work.
|
9
|
+
module Subcommand
|
10
|
+
class << self
|
11
|
+
def extended(mod)
|
12
|
+
mod.singleton_class.delegate :command_namespace, :commands,
|
13
|
+
:command_add, :command_subcommand_add, :command_prompt, :help, to: mod.class
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Subcommand-specific instance methods.
|
18
|
+
#
|
19
|
+
# Define Subcommand-specific method equivalents of the Command class
|
20
|
+
# methods needed to make this Subcommand instance unique.
|
21
|
+
|
22
|
+
# def command_namespace(namespace = nil)
|
23
|
+
# return @command_namespace || name if namespace.nil?
|
24
|
+
|
25
|
+
# @command_namespace = namespace
|
26
|
+
# end
|
27
|
+
|
28
|
+
# Subcommands can be used by any Command, so the :command_parent needs
|
29
|
+
# to be unique to this Subcommand instance.
|
30
|
+
def command_parent(parent = nil)
|
31
|
+
return @command_prompt if parent.nil?
|
32
|
+
|
33
|
+
@command_prompt = parent
|
34
|
+
end
|
35
|
+
|
36
|
+
def command_namespaces(namespaces = [])
|
37
|
+
command_parent&.command_namespaces(namespaces)
|
38
|
+
|
39
|
+
namespaces << command_namespace
|
40
|
+
namespaces
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colorize'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'yaml'
|
6
|
+
require_relative 'colorable'
|
7
|
+
require_relative 'folder_locations'
|
8
|
+
require_relative 'say'
|
9
|
+
|
10
|
+
module Dsu
|
11
|
+
module Support
|
12
|
+
module Configuration
|
13
|
+
include Colorable
|
14
|
+
include FolderLocations
|
15
|
+
include Say
|
16
|
+
|
17
|
+
CONFIG_FILENAME = '.dsu'
|
18
|
+
|
19
|
+
# rubocop:disable Style/StringHashKeys - YAML writing/loading necessitates this
|
20
|
+
DEFAULT_DSU_OPTIONS = {
|
21
|
+
# The order by which entries should be displayed by default:
|
22
|
+
# asc or desc, ascending or descending, respectively.
|
23
|
+
'entries_display_order' => 'desc',
|
24
|
+
'entries_file_name' => '%Y-%m-%d.json',
|
25
|
+
'entries_folder' => "#{FolderLocations.root_folder}/dsu/entries",
|
26
|
+
}.freeze
|
27
|
+
# rubocop:enable Style/StringHashKeys
|
28
|
+
|
29
|
+
def config_file
|
30
|
+
File.join(root_folder, CONFIG_FILENAME)
|
31
|
+
end
|
32
|
+
|
33
|
+
def config_file?
|
34
|
+
File.exist? config_file
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_config_file!
|
38
|
+
create_config_file config_file: config_file
|
39
|
+
print_config_file
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete_config_file!
|
43
|
+
delete_config_file config_file: config_file
|
44
|
+
end
|
45
|
+
|
46
|
+
def print_config_file
|
47
|
+
if config_file?
|
48
|
+
say "Config file (#{config_file}) contents:", SUCCESS
|
49
|
+
hash = YAML.safe_load(File.open(config_file))
|
50
|
+
say hash.to_yaml.gsub("\n-", "\n\n-"), SUCCESS
|
51
|
+
else
|
52
|
+
say "Config file (#{config_file}) does not exist.", WARNING
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def create_config_file(config_file:)
|
59
|
+
folder = File.dirname(config_file)
|
60
|
+
unless Dir.exist?(folder)
|
61
|
+
say "Destination folder for configuration file (#{folder}) does not exist", ERROR
|
62
|
+
return false
|
63
|
+
end
|
64
|
+
|
65
|
+
if File.exist?(config_file)
|
66
|
+
say "Configuration file (#{config_file}) already exists", WARNING
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
|
70
|
+
File.write(config_file, DEFAULT_DSU_OPTIONS.to_yaml)
|
71
|
+
say "Configuration file (#{config_file}) created.", SUCCESS
|
72
|
+
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
def delete_config_file(config_file:)
|
77
|
+
unless File.exist?(config_file)
|
78
|
+
say "Configuration file (#{config_file}) does not exist", WARNING
|
79
|
+
return false
|
80
|
+
end
|
81
|
+
|
82
|
+
File.delete config_file
|
83
|
+
say "Configuration file (#{config_file}) deleted", SUCCESS
|
84
|
+
|
85
|
+
true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../services/configuration_loader_service'
|
4
|
+
|
5
|
+
module Dsu
|
6
|
+
module Support
|
7
|
+
module EntryGroupFileable
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def entry_group_file_exists?
|
11
|
+
File.exist?(entry_group_file_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def entry_group_path_exists?
|
15
|
+
Dir.exist?(entries_folder)
|
16
|
+
end
|
17
|
+
|
18
|
+
def entry_group_file_path
|
19
|
+
File.join(entries_folder, entries_file_name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def entries_folder
|
23
|
+
@entries_folder ||= configuration[:entries_folder]
|
24
|
+
end
|
25
|
+
|
26
|
+
def entries_file_name
|
27
|
+
@entries_file_name ||= time.strftime(configuration[:entries_file_name])
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_entry_group_path_if!
|
31
|
+
FileUtils.mkdir_p(entries_folder) unless entry_group_path_exists?
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def configuration
|
37
|
+
@configuration ||= options[:configuration] || Services::ConfigurationLoaderService.new.call
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require_relative '../services/entry_group_reader_service'
|
5
|
+
require_relative '../models/entry'
|
6
|
+
|
7
|
+
module Dsu
|
8
|
+
module Support
|
9
|
+
module EntryGroupLoadable
|
10
|
+
module_function
|
11
|
+
|
12
|
+
# returns a Hash having :time and :entries
|
13
|
+
# where entries == an Array of Entry Hashes
|
14
|
+
# representing the JSON Entry objects for :time.
|
15
|
+
def entry_group_hash_for(time:)
|
16
|
+
entry_group_json = Services::EntryGroupReaderService.new(time: time).call
|
17
|
+
if entry_group_json.present?
|
18
|
+
return JSON.parse(entry_group_json, symbolize_names: true).tap do |hash|
|
19
|
+
hash[:time] = Time.parse(hash[:time])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
{
|
24
|
+
time: time,
|
25
|
+
entries: []
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Accepts an entry group hash and returns a
|
32
|
+
# hydrated entry group hash:
|
33
|
+
#
|
34
|
+
# {
|
35
|
+
# time: <Time object>,
|
36
|
+
# entries [
|
37
|
+
# <Entry object 0>,
|
38
|
+
# <Entry object 1>,
|
39
|
+
# ...
|
40
|
+
# ]
|
41
|
+
# }
|
42
|
+
def hydrate_entry_group_hash(entry_group_hash:, time:)
|
43
|
+
time = entry_group_hash.fetch(:time, time)
|
44
|
+
time = Time.parse(time) unless time.is_a? Time
|
45
|
+
entries = entry_group_hash.fetch(:entries, [])
|
46
|
+
entries = entries.map { |entry_hash| Models::Entry.new(**entry_hash) }
|
47
|
+
|
48
|
+
{ time: time, entries: entries }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
require 'colorize'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
module Dsu
|
8
|
+
module Support
|
9
|
+
module FolderLocations
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def root_folder
|
13
|
+
Dir.home
|
14
|
+
end
|
15
|
+
|
16
|
+
def temp_folder
|
17
|
+
Dir.tmpdir
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../ask'
|
4
|
+
require_relative '../colorable'
|
5
|
+
require_relative '../say'
|
6
|
+
|
7
|
+
module Dsu
|
8
|
+
module Interactive
|
9
|
+
class Cli
|
10
|
+
include Support::Colorable
|
11
|
+
include Support::Ask
|
12
|
+
include Support::Say
|
13
|
+
|
14
|
+
BACK_COMMANDS = %w[b].freeze
|
15
|
+
EXIT_COMMANDS = %w[x].freeze
|
16
|
+
HELP_COMMANDS = %w[?].freeze
|
17
|
+
PROMPT_TOKEN = '>'
|
18
|
+
|
19
|
+
attr_reader :name, :parent, :prompt
|
20
|
+
|
21
|
+
def initialize(name:, parent: nil, **options)
|
22
|
+
@name = name
|
23
|
+
@parent = parent
|
24
|
+
@prompt = options[:prompt]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Starts our interactive loop.
|
28
|
+
def start
|
29
|
+
help
|
30
|
+
process_commands
|
31
|
+
end
|
32
|
+
|
33
|
+
def process(command:)
|
34
|
+
if command.cancelled?
|
35
|
+
nil
|
36
|
+
elsif help?(command.command)
|
37
|
+
help
|
38
|
+
elsif back_or_exit?(command.command)
|
39
|
+
parent&.help
|
40
|
+
else
|
41
|
+
unrecognized_command command.command
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Dispays the full help; header and body.
|
46
|
+
def help
|
47
|
+
help_header
|
48
|
+
help_body
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# This is our interaction loop. Commands that are NOT help or
|
54
|
+
# back or exit commands are yielded to the subclass to execute. Help
|
55
|
+
# commands simply display help; back (or exit) commands transfer control
|
56
|
+
# back to the parent cli (if parent? is true) or exits the current
|
57
|
+
# cli (if parent? is false) respectfully.
|
58
|
+
def process_commands
|
59
|
+
loop do
|
60
|
+
command = wrap_command(ask)
|
61
|
+
process(command: command)
|
62
|
+
next if command.cancelled?
|
63
|
+
break if back_or_exit?(command.command)
|
64
|
+
end
|
65
|
+
say 'Done.', ABORTED unless parent?
|
66
|
+
end
|
67
|
+
|
68
|
+
def wrap_command(command)
|
69
|
+
Struct.new(:command, :args, :cancelled, keyword_init: true) do
|
70
|
+
def cancelled?
|
71
|
+
cancelled
|
72
|
+
end
|
73
|
+
|
74
|
+
def cancelled!
|
75
|
+
self[:cancelled] = true
|
76
|
+
end
|
77
|
+
end.new(
|
78
|
+
command: command.split[0],
|
79
|
+
args: command.split[1..],
|
80
|
+
cancelled: false
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
# This is the full prompt that needs to be displayed that includes
|
85
|
+
# all parent prompts, right down to the current prompt.
|
86
|
+
def full_prompt
|
87
|
+
prompts = full_prompt_build prompts: []
|
88
|
+
prompt_token = "#{PROMPT_TOKEN} "
|
89
|
+
"#{prompts.join prompt_token}#{prompt_token}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def parent?
|
93
|
+
!parent.nil?
|
94
|
+
end
|
95
|
+
|
96
|
+
def back?(command)
|
97
|
+
back_commands.include? command
|
98
|
+
end
|
99
|
+
|
100
|
+
def back_commands
|
101
|
+
@back_commands ||= BACK_COMMANDS
|
102
|
+
end
|
103
|
+
|
104
|
+
def exit?(command)
|
105
|
+
exit_commands.include? command
|
106
|
+
end
|
107
|
+
|
108
|
+
def exit_commands
|
109
|
+
@exit_commands ||= EXIT_COMMANDS
|
110
|
+
end
|
111
|
+
|
112
|
+
def back_or_exit?(command)
|
113
|
+
(back_commands + exit_commands).include? command
|
114
|
+
end
|
115
|
+
|
116
|
+
def help?(command)
|
117
|
+
help_commands.include? command
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns what are considered to be commands associated with
|
121
|
+
# displaying help.
|
122
|
+
def help_commands
|
123
|
+
@help_commands ||= HELP_COMMANDS
|
124
|
+
end
|
125
|
+
|
126
|
+
# Displays the help header; override this if you want to customize
|
127
|
+
# your own help header in your subclass.
|
128
|
+
def help_header
|
129
|
+
say "#{name} Help", HIGHLIGHT
|
130
|
+
say '---', HIGHLIGHT
|
131
|
+
end
|
132
|
+
|
133
|
+
# Override this in your subclass and call super AFTER you've
|
134
|
+
# displayed your subclass' help body.
|
135
|
+
def help_body
|
136
|
+
say "[#{HELP_COMMANDS.join(' | ')}] Display help", HIGHLIGHT
|
137
|
+
say "[#{BACK_COMMANDS.join(' | ')}] Go back", HIGHLIGHT if parent?
|
138
|
+
say "[#{EXIT_COMMANDS.join(' | ')}] Exit", HIGHLIGHT unless parent?
|
139
|
+
end
|
140
|
+
|
141
|
+
# This simply outputs our prompt and accepts user input.
|
142
|
+
def ask
|
143
|
+
super full_prompt
|
144
|
+
end
|
145
|
+
|
146
|
+
def unrecognized_command(command)
|
147
|
+
say "Unrecognized command (\"#{command}\"). Try again.", ERROR
|
148
|
+
end
|
149
|
+
|
150
|
+
# Builds the full prompt to be used which amounts to:
|
151
|
+
# <parent cli prompt> PROMPT_TOKEN <child cli 1 prompt>
|
152
|
+
# PROMPT_TOKEN <child 2 cli prompt> ...
|
153
|
+
def full_prompt_build(prompts:)
|
154
|
+
parent.send(:full_prompt_build, prompts: prompts) if parent?
|
155
|
+
|
156
|
+
prompts << prompt
|
157
|
+
prompts.flatten
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colorized_string'
|
4
|
+
|
5
|
+
module Dsu
|
6
|
+
module Support
|
7
|
+
module Say
|
8
|
+
class << self
|
9
|
+
def say(text, color = nil)
|
10
|
+
puts say_string_for(text, color)
|
11
|
+
end
|
12
|
+
|
13
|
+
def say_string_for(text, color = nil)
|
14
|
+
unless color.nil? || color.is_a?(Symbol)
|
15
|
+
raise ':color is the wrong type. "Symbol" was expected, but ' \
|
16
|
+
"\"#{color.class}\" was returned."
|
17
|
+
end
|
18
|
+
|
19
|
+
return text if color.nil?
|
20
|
+
|
21
|
+
text.public_send(color)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def say(text, color = nil)
|
26
|
+
Say.say(text, color)
|
27
|
+
end
|
28
|
+
|
29
|
+
# NOTE: some modes (ColorizedString.modes) will cancel out each other if
|
30
|
+
# overriden in a block. For example, if you set a string to be bold
|
31
|
+
# (i.e. mode: :bold) and then override it in a block (e.g. string.underline)
|
32
|
+
# the string will not be bold and underlined, it will just be underlined.
|
33
|
+
def colorize_string(string:, color: :default, mode: :default)
|
34
|
+
colorized_string = ColorizedString[string].colorize(color: color, mode: mode)
|
35
|
+
colorized_string = yield colorized_string if block_given?
|
36
|
+
colorized_string
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'active_support/core_ext/numeric/time'
|
5
|
+
|
6
|
+
module Dsu
|
7
|
+
module Support
|
8
|
+
# This module provides functions for formatting Time objects
|
9
|
+
# to display in the console.
|
10
|
+
module TimeFormatable
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def formatted_time(time:)
|
14
|
+
time = time.localtime if time.utc?
|
15
|
+
|
16
|
+
today_yesterday_or_tomorrow = if today?(time: time)
|
17
|
+
'Today'
|
18
|
+
elsif yesterday?(time: time)
|
19
|
+
'Yesterday'
|
20
|
+
elsif tomorrow?(time: time)
|
21
|
+
'Tomorrow'
|
22
|
+
end
|
23
|
+
|
24
|
+
return time.strftime('%A, %Y-%m-%d') unless today_yesterday_or_tomorrow
|
25
|
+
|
26
|
+
time.strftime("%A, (#{today_yesterday_or_tomorrow}) %Y-%m-%d")
|
27
|
+
end
|
28
|
+
|
29
|
+
def today?(time:)
|
30
|
+
time.strftime('%Y%m%d') == Time.now.strftime('%Y%m%d')
|
31
|
+
end
|
32
|
+
|
33
|
+
def yesterday?(time:)
|
34
|
+
time.strftime('%Y%m%d') == 1.day.ago(Time.now).strftime('%Y%m%d')
|
35
|
+
end
|
36
|
+
|
37
|
+
def tomorrow?(time:)
|
38
|
+
time.strftime('%Y%m%d') == 1.day.from_now(Time.now).strftime('%Y%m%d')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|