dsu 0.1.0.alpha.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.
- 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
|