dsu 2.4.3 → 3.0.0.alpha.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -0
  3. data/CHANGELOG.md +181 -204
  4. data/Gemfile.lock +13 -13
  5. data/README.md +7 -8
  6. data/Rakefile +6 -0
  7. data/current_project.bak +4 -0
  8. data/lib/dsu/cli.rb +24 -6
  9. data/lib/dsu/crud/json_file.rb +3 -0
  10. data/lib/dsu/migration/version.rb +1 -1
  11. data/lib/dsu/models/color_theme.rb +7 -58
  12. data/lib/dsu/models/configuration.rb +18 -3
  13. data/lib/dsu/models/entry_group.rb +0 -7
  14. data/lib/dsu/models/migration_version.rb +0 -1
  15. data/lib/dsu/models/project.rb +295 -0
  16. data/lib/dsu/presenters/base_presenter_ex.rb +1 -12
  17. data/lib/dsu/presenters/export/all_presenter.rb +14 -19
  18. data/lib/dsu/presenters/export/dates_presenter.rb +17 -20
  19. data/lib/dsu/presenters/import/all_presenter.rb +20 -25
  20. data/lib/dsu/presenters/import/dates_presenter.rb +25 -27
  21. data/lib/dsu/presenters/import/import_entry.rb +22 -0
  22. data/lib/dsu/presenters/import/import_file.rb +9 -1
  23. data/lib/dsu/presenters/project/create_presenter.rb +44 -0
  24. data/lib/dsu/presenters/project/delete_by_number_presenter.rb +54 -0
  25. data/lib/dsu/presenters/project/delete_presenter.rb +53 -0
  26. data/lib/dsu/presenters/project/list_presenter.rb +24 -0
  27. data/lib/dsu/presenters/project/rename_by_number_presenter.rb +63 -0
  28. data/lib/dsu/presenters/project/rename_presenter.rb +57 -0
  29. data/lib/dsu/presenters/project/use_by_number_presenter.rb +53 -0
  30. data/lib/dsu/presenters/project/use_presenter.rb +52 -0
  31. data/lib/dsu/services/entry_group/exporter_service.rb +22 -5
  32. data/lib/dsu/services/entry_group/importer_service.rb +41 -8
  33. data/lib/dsu/services/project/hydrator_service.rb +40 -0
  34. data/lib/dsu/services/project/rename_service.rb +70 -0
  35. data/lib/dsu/subcommands/export.rb +4 -2
  36. data/lib/dsu/subcommands/import.rb +7 -3
  37. data/lib/dsu/subcommands/project.rb +149 -0
  38. data/lib/dsu/support/ask.rb +10 -3
  39. data/lib/dsu/support/color_themable.rb +1 -1
  40. data/lib/dsu/support/command_hookable.rb +7 -2
  41. data/lib/dsu/support/descriptable.rb +5 -21
  42. data/lib/dsu/support/fileable.rb +39 -1
  43. data/lib/dsu/support/project_file_system.rb +121 -0
  44. data/lib/dsu/support/short_string.rb +24 -0
  45. data/lib/dsu/support/time_comparable.rb +2 -0
  46. data/lib/dsu/support/transform_project_name.rb +24 -0
  47. data/lib/dsu/validators/project_name_validator.rb +58 -0
  48. data/lib/dsu/version.rb +1 -1
  49. data/lib/dsu/views/base_list_view.rb +41 -0
  50. data/lib/dsu/views/export.rb +60 -6
  51. data/lib/dsu/views/import.rb +83 -7
  52. data/lib/dsu/views/import_dates.rb +17 -0
  53. data/lib/dsu/views/project/create.rb +87 -0
  54. data/lib/dsu/views/project/delete.rb +96 -0
  55. data/lib/dsu/views/project/delete_by_number.rb +19 -0
  56. data/lib/dsu/views/project/list.rb +115 -0
  57. data/lib/dsu/views/project/rename.rb +98 -0
  58. data/lib/dsu/views/project/rename_by_number.rb +21 -0
  59. data/lib/dsu/views/project/use.rb +97 -0
  60. data/lib/dsu/views/project/use_by_number.rb +19 -0
  61. data/lib/dsu.rb +2 -10
  62. data/lib/locales/en/active_record.yml +9 -0
  63. data/lib/locales/en/commands.yml +9 -3
  64. data/lib/locales/en/miscellaneous.yml +4 -0
  65. data/lib/locales/en/services.yml +4 -0
  66. data/lib/locales/en/subcommands.yml +247 -15
  67. data/project.bak +0 -0
  68. metadata +34 -9
  69. data/lib/dsu/presenters/export/messages.rb +0 -32
  70. data/lib/dsu/presenters/export/nothing_to_export.rb +0 -13
  71. data/lib/dsu/presenters/export/service_callable.rb +0 -20
  72. data/lib/dsu/presenters/import/messages.rb +0 -42
  73. data/lib/dsu/presenters/import/service_callable.rb +0 -21
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/project'
4
+ require_relative '../base_presenter_ex'
5
+
6
+ module Dsu
7
+ module Presenters
8
+ module Project
9
+ class UseByNumberPresenter < BasePresenterEx
10
+ attr_reader :project_number
11
+
12
+ delegate :project_name, to: :project, allow_nil: true
13
+ delegate :description, to: :project, prefix: true, allow_nil: true
14
+
15
+ def initialize(project_number:, options: {})
16
+ super(options: options)
17
+
18
+ raise ArgumentError, 'project_number is blank' if project_number.blank?
19
+
20
+ self.project_number = project_number
21
+ end
22
+
23
+ def respond(response:)
24
+ return false unless response
25
+
26
+ project.use! if project&.present?
27
+ end
28
+
29
+ def already_current_project?
30
+ project&.current_project?
31
+ end
32
+
33
+ def project_does_not_exist?
34
+ !project&.exist?
35
+ end
36
+
37
+ def project_errors
38
+ return false unless project&.persisted?
39
+
40
+ project.errors.full_messages
41
+ end
42
+
43
+ private
44
+
45
+ attr_writer :project_number
46
+
47
+ def project
48
+ @project ||= Models::Project.find_by_number(project_number: project_number)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/project'
4
+ require_relative '../base_presenter_ex'
5
+
6
+ module Dsu
7
+ module Presenters
8
+ module Project
9
+ class UsePresenter < BasePresenterEx
10
+ attr_reader :project_name
11
+
12
+ delegate :description, to: :project, prefix: true, allow_nil: true
13
+
14
+ def initialize(project_name:, options: {})
15
+ super(options: options)
16
+
17
+ raise ArgumentError, 'project_name is blank' if project_name.blank?
18
+
19
+ self.project_name = project_name
20
+ end
21
+
22
+ def respond(response:)
23
+ return false unless response
24
+
25
+ project.use! if project&.present?
26
+ end
27
+
28
+ def already_current_project?
29
+ project&.current_project?
30
+ end
31
+
32
+ def project_does_not_exist?
33
+ !project.exist?
34
+ end
35
+
36
+ def project_errors
37
+ return false unless project.persisted?
38
+
39
+ project.errors.full_messages
40
+ end
41
+
42
+ private
43
+
44
+ attr_writer :project_name
45
+
46
+ def project
47
+ @project ||= Models::Project.find_or_initialize(project_name: project_name)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -3,26 +3,34 @@
3
3
  require 'csv'
4
4
  require_relative '../../models/entry_group'
5
5
  require_relative '../../support/fileable'
6
+ require_relative '../../support/transform_project_name'
6
7
 
7
8
  module Dsu
8
9
  module Services
9
10
  module EntryGroup
11
+ # ExporterService exports entry groups to a CSV file.
12
+ # NOTE: This class exports all entries passed to it. It does not filter
13
+ # entries based on optional options[:time] that may be passed to it.
14
+ # Rather, times are used to determine the export file name only.
10
15
  class ExporterService
11
16
  include Support::Fileable
17
+ include Support::TransformProjectName
12
18
 
13
- def initialize(entry_groups:, options: {})
19
+ def initialize(project_name:, entry_groups:, options: {})
20
+ raise ArgumentError, 'Argument project_name is blank' if project_name.blank?
14
21
  raise ArgumentError, 'Argument entry_groups is blank' if entry_groups.blank?
15
22
  raise ArgumentError, 'Argument entry_groups are not all valid' unless entry_groups.all?(&:valid?)
16
23
 
17
24
  validate_entry_group_entries_present! entry_groups
18
25
 
26
+ @project_name = project_name
19
27
  @entry_groups = entry_groups
20
28
  @options = options
21
29
  end
22
30
 
23
31
  def call
24
32
  CSV.open(export_file_path, 'w') do |csv|
25
- csv << %i[version entry_group entry_no total_entries entry_group_entry]
33
+ csv << %i[project_name version entry_group entry_no total_entries entry_group_entry]
26
34
 
27
35
  entry_groups.each do |entry_group|
28
36
  export_entry_group(entry_group: entry_group, csv: csv)
@@ -38,10 +46,11 @@ module Dsu
38
46
 
39
47
  private
40
48
 
41
- attr_reader :entry_groups, :options
49
+ attr_reader :project_name, :entry_groups, :options
42
50
 
43
51
  def export_entry_data(entry_group:, entry:, entry_index:)
44
52
  [
53
+ project_name,
45
54
  entry_group.version,
46
55
  entry_group.time.to_date,
47
56
  entry_index + 1,
@@ -57,11 +66,19 @@ module Dsu
57
66
  end
58
67
 
59
68
  def export_file_name
60
- "dsu-#{timestamp}-#{times.min.to_date}-thru-#{times.max.to_date}.csv"
69
+ transformed_file_name = transform_project_name project_name, options: options
70
+ "dsu-export-#{transformed_file_name}-" \
71
+ "#{timestamp}-#{export_scope}-#{times.min.to_date}-thru-#{times.max.to_date}.csv"
72
+ end
73
+
74
+ def export_scope
75
+ return 'all-entry-groups' unless options.fetch(:times, nil)
76
+
77
+ 'entry-groups'
61
78
  end
62
79
 
63
80
  def times
64
- @times ||= entry_groups.map(&:time)
81
+ @times ||= options.fetch(:times, entry_groups.map(&:time))
65
82
  end
66
83
 
67
84
  def timestamp
@@ -2,23 +2,34 @@
2
2
 
3
3
  require 'csv'
4
4
  require_relative '../../models/entry_group'
5
+ require_relative '../../models/project'
5
6
 
6
7
  module Dsu
7
8
  module Services
8
9
  module EntryGroup
9
10
  # Expects a hash having the following format:
10
11
  # {
11
- # "2023-12-29" => ["Entry 1 description", "Entry 2 description", ...],
12
- # "2023-12-30" => ["Entry 1 description", ...],
13
- # "2023-12-31" => ["Entry 1 description", ...]
12
+ # "Project 1 Name" => {
13
+ # "2023-12-29" => ["Entry 1 description", "Entry 2 description", ...],
14
+ # "2023-12-30" => ["Entry 1 description", ...],
15
+ # "2023-12-31" => ["Entry 1 description", ...]
16
+ # },
17
+ # "Project 2 Name" => {
18
+ # "2023-12-29" => ["Entry 1 description", "Entry 2 description", ...],
19
+ # "2023-12-30" => ["Entry 1 description", ...],
20
+ # "2023-12-31" => ["Entry 1 description", ...]
21
+ # }
14
22
  # }
15
23
  class ImporterService
16
24
  include Support::Fileable
17
25
 
18
- def initialize(import_entry_groups:, options: {})
19
- raise ArgumentError, 'Argument import_entry_groups is blank' if import_entry_groups.blank?
26
+ def initialize(import_projects:, options: {})
27
+ raise ArgumentError, 'Argument import_projects is blank' if import_projects.blank?
28
+ raise ArgumentError, 'Argument import_projects is not a Hash' unless import_projects.is_a?(Hash)
20
29
 
21
- @import_entry_groups = import_entry_groups
30
+ raise_if_more_than_one_project(import_projects)
31
+
32
+ @import_projects = import_projects
22
33
  @options = options
23
34
  end
24
35
 
@@ -28,10 +39,10 @@ module Dsu
28
39
 
29
40
  private
30
41
 
31
- attr_reader :import_entry_groups, :options
42
+ attr_reader :import_projects, :options
32
43
 
33
44
  def import!
34
- import_entry_groups.each_pair do |entry_group_date, entry_descriptions|
45
+ project_entry_groups.each_pair do |entry_group_date, entry_descriptions|
35
46
  entry_group_for(entry_group_date).tap do |entry_group|
36
47
  entry_descriptions.each do |entry_description|
37
48
  add_entry_group_entry_if(entry_group: entry_group, entry_description: entry_description)
@@ -67,6 +78,14 @@ module Dsu
67
78
  entry_group.entries << entry
68
79
  end
69
80
 
81
+ def project_entry_groups
82
+ @project_entry_groups ||= if override_project?
83
+ import_projects.values.first || {}
84
+ else
85
+ import_projects.fetch(current_project_name, {})
86
+ end
87
+ end
88
+
70
89
  def merge?
71
90
  options.fetch(:merge, true)
72
91
  end
@@ -75,9 +94,23 @@ module Dsu
75
94
  !merge?
76
95
  end
77
96
 
97
+ def override_project?
98
+ options.fetch(:override, false)
99
+ end
100
+
78
101
  def import_messages
79
102
  @import_messages ||= {}
80
103
  end
104
+
105
+ def current_project_name
106
+ @current_project_name ||= Models::Project.current_project.project_name
107
+ end
108
+
109
+ def raise_if_more_than_one_project(import_projects)
110
+ return if import_projects.keys.one?
111
+
112
+ raise ArgumentError, 'Only one project can be imported at a time'
113
+ end
81
114
  end
82
115
  end
83
116
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/color_theme'
4
+
5
+ module Dsu
6
+ module Services
7
+ module Project
8
+ class HydratorService
9
+ def initialize(project_hash:, options: {})
10
+ raise ArgumentError, 'project_hash is nil' if project_hash.nil?
11
+
12
+ unless project_hash.is_a?(Hash)
13
+ raise ArgumentError,
14
+ "project_hash is the wrong object type: \"#{project_hash}\""
15
+ end
16
+ raise ArgumentError, 'options is nil' if options.nil?
17
+ raise ArgumentError, "options is the wrong object type:\"#{options}\"" unless options.is_a?(Hash)
18
+
19
+ @project_hash = project_hash
20
+ @options = options || {}
21
+ end
22
+
23
+ def call
24
+ Models::Project.new(**hydrate)
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :project_hash, :options
30
+
31
+ # Not much going on here at all, but it's here for consistency.
32
+ # Perform any pre-processing of the project_hash here (e.g. symbolize keys,
33
+ # convert values to the correct type, etc.).
34
+ def hydrate
35
+ project_hash
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/project'
4
+ require_relative '../../support/fileable'
5
+
6
+ module Dsu
7
+ module Services
8
+ module Project
9
+ class RenameService
10
+ def initialize(from_project_name:, to_project_name:, to_project_description:, options: {})
11
+ @from_project_name = from_project_name
12
+ @to_project_name = to_project_name
13
+ @to_project_description = to_project_description
14
+ @options = options
15
+ end
16
+
17
+ def call
18
+ validate!
19
+
20
+ # NOTE: The default and current states need to be captured before
21
+ # the project is renamed.
22
+ rename!(
23
+ make_default: Models::Project.default_project?(project_name: from_project_name),
24
+ make_current: Models::Project.current_project?(project_name: from_project_name)
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :from_project_name, :to_project_name, :to_project_description, :options
31
+
32
+ def rename!(make_default:, make_current:)
33
+ move_project
34
+
35
+ Models::Project.update(project_name: to_project_name,
36
+ description: to_project_description, options: options).tap do |project|
37
+ project.default! if make_default
38
+ project.use! if make_current
39
+ end
40
+ end
41
+
42
+ def move_project
43
+ FileUtils.mv(Support::Fileable.project_folder_for(project_name: from_project_name), temp_project_folder)
44
+ FileUtils.mv(temp_project_folder, Support::Fileable.project_folder_for(project_name: to_project_name))
45
+ end
46
+
47
+ def temp_project_folder
48
+ @temp_project_folder ||= Support::Fileable.project_folder_for(project_name: SecureRandom.uuid)
49
+ end
50
+
51
+ def validate!
52
+ validate_from_project_name!
53
+ validate_to_project_name!
54
+ end
55
+
56
+ def validate_from_project_name!
57
+ unless Models::Project.project_file_exist?(project_name: from_project_name)
58
+ raise I18n.t('models.project.errors.does_not_exist', project_name: from_project_name)
59
+ end
60
+ end
61
+
62
+ def validate_to_project_name!
63
+ if Models::Project.project_file_exist?(project_name: to_project_name)
64
+ raise I18n.t('models.project.errors.new_project_already_exists', project_name: to_project_name)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -23,7 +23,8 @@ module Dsu
23
23
  long_desc I18n.t('subcommands.export.all.long_desc')
24
24
  option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
25
25
  def all
26
- Views::Export.new(presenter: all_presenter(options: options)).render
26
+ options = configuration.to_h.merge(self.options).with_indifferent_access
27
+ Views::Export.new(presenter: all_presenter(options: options), options: options).render
27
28
  end
28
29
 
29
30
  desc I18n.t('subcommands.export.dates.desc'), I18n.t('subcommands.export.dates.usage')
@@ -41,7 +42,8 @@ module Dsu
41
42
  return
42
43
  end
43
44
 
44
- Views::Export.new(presenter: dates_presenter_for(from: times.min, to: times.max, options: options)).render
45
+ Views::Export.new(presenter:
46
+ dates_presenter_for(from: times.min, to: times.max, options: options), options: options).render
45
47
  rescue ArgumentError => e
46
48
  Views::Shared::Error.new(messages: e.message).render
47
49
  end
@@ -6,6 +6,7 @@ require_relative '../support/command_options/dsu_times'
6
6
  require_relative '../support/command_options/time_mnemonic'
7
7
  require_relative '../support/time_formatable'
8
8
  require_relative '../views/import'
9
+ require_relative '../views/import_dates'
9
10
  require_relative '../views/shared/error'
10
11
  require_relative 'base_subcommand'
11
12
 
@@ -23,10 +24,12 @@ module Dsu
23
24
  long_desc I18n.t('subcommands.import.all.long_desc')
24
25
  option :import_file, type: :string, required: true, aliases: '-i', banner: 'IMPORT_CVS_FILE'
25
26
  option :merge, type: :boolean, default: true, aliases: '-m'
27
+ option :override, type: :boolean, default: false, aliases: '-o'
26
28
  option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
27
29
  def all
30
+ options = configuration.to_h.merge(self.options).with_indifferent_access
28
31
  Views::Import.new(presenter: all_presenter(import_file_path: options[:import_file],
29
- options: options)).render
32
+ options: options), options: options).render
30
33
  end
31
34
 
32
35
  desc I18n.t('subcommands.import.dates.desc'), I18n.t('subcommands.import.dates.usage')
@@ -37,6 +40,7 @@ module Dsu
37
40
  option :to, type: :string, required: true, aliases: '-t', banner: 'DATE|MNEMONIC'
38
41
  option :import_file, type: :string, required: true, aliases: '-i', banner: 'IMPORT_CVS_FILE'
39
42
  option :merge, type: :boolean, default: true, aliases: '-m'
43
+ option :override, type: :boolean, default: false, aliases: '-o'
40
44
  option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
41
45
  def dates
42
46
  options = configuration.to_h.merge(self.options).with_indifferent_access
@@ -46,10 +50,10 @@ module Dsu
46
50
  return
47
51
  end
48
52
 
49
- Views::Import.new(presenter: dates_presenter_for(from: times.min,
53
+ Views::ImportDates.new(presenter: dates_presenter_for(from: times.min,
50
54
  to: times.max,
51
55
  import_file_path: options[:import_file],
52
- options: options)).render
56
+ options: options), options: options).render
53
57
  rescue ArgumentError => e
54
58
  Views::Shared::Error.new(messages: e.message).render
55
59
  end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../presenters/project/create_presenter'
4
+ require_relative '../presenters/project/delete_presenter'
5
+ require_relative '../presenters/project/list_presenter'
6
+ require_relative '../presenters/project/use_by_number_presenter'
7
+ require_relative '../presenters/project/use_presenter'
8
+ require_relative '../views/project/create'
9
+ require_relative '../views/project/use'
10
+ require_relative '../views/project/use_by_number'
11
+ require_relative '../views/shared/error'
12
+ require_relative 'base_subcommand'
13
+
14
+ module Dsu
15
+ module Subcommands
16
+ class Project < BaseSubcommand
17
+ # TODO: I18n.
18
+ map %w[c] => :create
19
+ map %w[d] => :delete
20
+ map %w[l] => :list
21
+ map %w[r] => :rename
22
+ map %w[u] => :use
23
+
24
+ desc I18n.t('subcommands.project.create.desc'), I18n.t('subcommands.project.create.usage')
25
+ long_desc I18n.t('subcommands.project.create.long_desc')
26
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
27
+ def create(project_name = nil, description = nil)
28
+ project_name = project_name.to_s.strip
29
+ description = description.to_s.strip
30
+ if project_name.blank?
31
+ return Views::Shared::Error.new(
32
+ messages: I18n.t('subcommands.project.messages.project_name_blank')
33
+ ).render
34
+ end
35
+
36
+ options = configuration.to_h.merge(self.options).with_indifferent_access
37
+ presenter = Presenters::Project::CreatePresenter.new(project_name: project_name,
38
+ description: description, options: options)
39
+ Views::Project::Create.new(presenter: presenter, options: options).render
40
+ end
41
+
42
+ desc I18n.t('subcommands.project.delete.desc'), I18n.t('subcommands.project.delete.usage')
43
+ long_desc I18n.t('subcommands.project.delete.long_desc')
44
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
45
+ def delete(project_name_or_number = nil)
46
+ options = configuration.to_h.merge(self.options).with_indifferent_access
47
+ presenter = delete_presenter_for(project_name_or_number, options: options)
48
+ delete_view_for(project_name_or_number, presenter: presenter, options: options).render
49
+ end
50
+
51
+ desc I18n.t('subcommands.project.list.desc'), I18n.t('subcommands.project.list.usage')
52
+ long_desc I18n.t('subcommands.project.list.long_desc')
53
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
54
+ def list
55
+ options = configuration.to_h.merge(self.options).with_indifferent_access
56
+ presenter = Presenters::Project::ListPresenter.new(options: options)
57
+ Views::Project::List.new(presenter: presenter, options: options).render
58
+ end
59
+
60
+ desc I18n.t('subcommands.project.rename.desc'), I18n.t('subcommands.project.rename.usage')
61
+ long_desc I18n.t('subcommands.project.rename.long_desc')
62
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
63
+ def rename(project_name_or_number = nil, new_project_name = nil, new_project_description = nil)
64
+ project_name_or_number = project_name_or_number.to_s.strip
65
+ new_project_name = new_project_name&.to_s&.strip
66
+ new_project_description = new_project_description&.to_s&.strip
67
+
68
+ if new_project_name.blank?
69
+ return Views::Shared::Error.new(
70
+ messages: I18n.t('subcommands.project.messages.new_project_name_blank')
71
+ ).render
72
+ end
73
+
74
+ options = configuration.to_h.merge(self.options).with_indifferent_access
75
+ presenter = rename_presenter_for(project_name_or_number, new_project_name: new_project_name,
76
+ new_project_description: new_project_description, options: options)
77
+ rename_view_for(project_name_or_number, presenter: presenter, options: options).render
78
+ end
79
+
80
+ desc I18n.t('subcommands.project.use.desc'), I18n.t('subcommands.project.use.usage')
81
+ long_desc I18n.t('subcommands.project.use.long_desc')
82
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
83
+ def use(project_name_or_number = nil)
84
+ options = configuration.to_h.merge(self.options).with_indifferent_access
85
+ presenter = use_presenter_for(project_name_or_number, options: options)
86
+ use_view_for(project_name_or_number, presenter: presenter, options: options).render
87
+ end
88
+
89
+ private
90
+
91
+ def delete_view_for(project_name, presenter:, options:)
92
+ if project_number?(project_name)
93
+ Views::Project::DeleteByNumber.new(presenter: presenter, options: options)
94
+ else
95
+ Views::Project::Delete.new(presenter: presenter, options: options)
96
+ end
97
+ end
98
+
99
+ def delete_presenter_for(project_name, options:)
100
+ if project_number?(project_name)
101
+ Presenters::Project::DeleteByNumberPresenter.new(project_number: project_name.to_i, options: options)
102
+ else
103
+ project_name = Models::Project.default_project_name if project_name.blank?
104
+ Presenters::Project::DeletePresenter.new(project_name: project_name, options: options)
105
+ end
106
+ end
107
+
108
+ def rename_view_for(project_name, presenter:, options:)
109
+ if project_number?(project_name)
110
+ Views::Project::RenameByNumber.new(presenter: presenter, options: options)
111
+ else
112
+ Views::Project::Rename.new(presenter: presenter, options: options)
113
+ end
114
+ end
115
+
116
+ def rename_presenter_for(project_name, new_project_name:, new_project_description:, options:)
117
+ if project_number?(project_name)
118
+ Presenters::Project::RenameByNumberPresenter.new(project_number: project_name.to_i,
119
+ new_project_name: new_project_name, new_project_description: new_project_description, options: options)
120
+ else
121
+ project_name = Models::Project.default_project_name if project_name.blank?
122
+ Presenters::Project::RenamePresenter.new(project_name: project_name,
123
+ new_project_name: new_project_name, new_project_description: new_project_description, options: options)
124
+ end
125
+ end
126
+
127
+ def use_view_for(project_name, presenter:, options:)
128
+ if project_number?(project_name)
129
+ Views::Project::UseByNumber.new(presenter: presenter, options: options)
130
+ else
131
+ Views::Project::Use.new(presenter: presenter, options: options)
132
+ end
133
+ end
134
+
135
+ def use_presenter_for(project_name, options:)
136
+ if project_number?(project_name)
137
+ Presenters::Project::UseByNumberPresenter.new(project_number: project_name.to_i, options: options)
138
+ else
139
+ project_name = Models::Project.default_project_name if project_name.blank?
140
+ Presenters::Project::UsePresenter.new(project_name: project_name, options: options)
141
+ end
142
+ end
143
+
144
+ def project_number?(project_name)
145
+ /^[+-]?\d+(\.\d+)?$/.match?(project_name.to_s)
146
+ end
147
+ end
148
+ end
149
+ end
@@ -1,13 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'io/console'
3
4
  require 'thor'
4
5
 
5
6
  module Dsu
6
7
  module Support
7
8
  module Ask
8
- def ask(prompt)
9
- options = {}
10
- Thor::LineEditor.readline(prompt, options)
9
+ def ask_while(prompt, options: {}) # rubocop:disable Lint/UnusedMethodArgument
10
+ loop do
11
+ print prompt
12
+ char = $stdin.getch
13
+ puts char
14
+ return char if yield(char)
15
+
16
+ char
17
+ end
11
18
  end
12
19
 
13
20
  def yes?(prompt, options: {})
@@ -8,7 +8,7 @@ module Dsu
8
8
  def prompt_with_options(prompt:, options:)
9
9
  # HACK: This module needs to be refactored to be more generic.
10
10
  target_color_theme = defined?(color_theme) ? color_theme : self
11
- options = "[#{options.join('/')}]"
11
+ options = "[#{options.join(',')}]"
12
12
  "#{apply_theme(prompt, theme_color: target_color_theme.prompt)} " \
13
13
  "#{apply_theme(options, theme_color: target_color_theme.prompt_options)}" \
14
14
  "#{apply_theme('>', theme_color: target_color_theme.prompt)}"
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../env'
4
4
  require_relative '../models/color_theme'
5
+ require_relative '../models/project'
5
6
  require_relative '../services/stderr_redirector_service'
6
7
  require_relative '../views/shared/error'
7
8
  require_relative 'color_themable'
@@ -34,14 +35,18 @@ module Dsu
34
35
  end
35
36
 
36
37
  def display_dsu_footer
37
- puts apply_theme('_' * 35, theme_color: color_theme.dsu_footer)
38
+ puts apply_theme('_' * 50, theme_color: color_theme.dsu_footer)
38
39
  # TODO: I18n.
39
- puts apply_theme("dsu | Version: #{Dsu::VERSION} | Theme: #{color_theme.theme_name}",
40
+ puts apply_theme("dsu v#{Dsu::VERSION} | Project: #{project} | Theme: #{color_theme.theme_name}",
40
41
  theme_color: color_theme.dsu_footer)
41
42
  end
42
43
 
43
44
  private
44
45
 
46
+ def project
47
+ Models::Project.current_project_name
48
+ end
49
+
45
50
  def suspend_header?(args, _options)
46
51
  return false unless args.count > 1
47
52