doto 0.0.1.pre.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.
Files changed (179) hide show
  1. checksums.yaml +7 -0
  2. data/.env.test +1 -0
  3. data/.reek.yml +20 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +206 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +7 -0
  8. data/CODE_OF_CONDUCT.md +84 -0
  9. data/Gemfile +30 -0
  10. data/Gemfile.lock +179 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +38 -0
  13. data/Rakefile +16 -0
  14. data/bin/console +36 -0
  15. data/bin/doto +3 -0
  16. data/bin/setup +18 -0
  17. data/exe/doto +33 -0
  18. data/lib/core/ruby/color_theme_colors.rb +16 -0
  19. data/lib/core/ruby/color_theme_mode.rb +42 -0
  20. data/lib/core/ruby/wrap_and_join.rb +31 -0
  21. data/lib/doto/base_cli.rb +56 -0
  22. data/lib/doto/cli.rb +131 -0
  23. data/lib/doto/command_services/add_entry_service.rb +50 -0
  24. data/lib/doto/crud/json_file.rb +161 -0
  25. data/lib/doto/env.rb +44 -0
  26. data/lib/doto/migration/base_service.rb +118 -0
  27. data/lib/doto/migration/migrator.rb +24 -0
  28. data/lib/doto/migration/raw_helpers/color_theme_hash.rb +13 -0
  29. data/lib/doto/migration/raw_helpers/configuration_hash.rb +15 -0
  30. data/lib/doto/migration/raw_helpers/entry_group_hash.rb +13 -0
  31. data/lib/doto/migration/raw_json_file.rb +15 -0
  32. data/lib/doto/migration/raw_json_files.rb +56 -0
  33. data/lib/doto/migration/v20230613121411/service.rb +94 -0
  34. data/lib/doto/migration/v20240210161248/service.rb +148 -0
  35. data/lib/doto/migration/version.rb +7 -0
  36. data/lib/doto/models/color_theme.rb +224 -0
  37. data/lib/doto/models/configuration.rb +185 -0
  38. data/lib/doto/models/entry.rb +63 -0
  39. data/lib/doto/models/entry_group.rb +223 -0
  40. data/lib/doto/models/migration_version.rb +49 -0
  41. data/lib/doto/models/project.rb +295 -0
  42. data/lib/doto/presenters/base_presenter.rb +32 -0
  43. data/lib/doto/presenters/base_presenter_ex.rb +15 -0
  44. data/lib/doto/presenters/color_theme_presenter.rb +52 -0
  45. data/lib/doto/presenters/color_theme_show_presenter.rb +55 -0
  46. data/lib/doto/presenters/configuration_presenter.rb +50 -0
  47. data/lib/doto/presenters/entry_group/list/date_presenter.rb +77 -0
  48. data/lib/doto/presenters/entry_group/list/dates_presenter.rb +60 -0
  49. data/lib/doto/presenters/entry_group/list/messages.rb +15 -0
  50. data/lib/doto/presenters/entry_group/list/nothing_to_list.rb +15 -0
  51. data/lib/doto/presenters/entry_group_presenter.rb +35 -0
  52. data/lib/doto/presenters/entry_presenter.rb +25 -0
  53. data/lib/doto/presenters/export/all_presenter.rb +44 -0
  54. data/lib/doto/presenters/export/dates_presenter.rb +55 -0
  55. data/lib/doto/presenters/import/all_presenter.rb +57 -0
  56. data/lib/doto/presenters/import/dates_presenter.rb +70 -0
  57. data/lib/doto/presenters/import/import_entry.rb +22 -0
  58. data/lib/doto/presenters/import/import_file.rb +33 -0
  59. data/lib/doto/presenters/project/create_presenter.rb +44 -0
  60. data/lib/doto/presenters/project/defaultable.rb +15 -0
  61. data/lib/doto/presenters/project/delete_by_number_presenter.rb +54 -0
  62. data/lib/doto/presenters/project/delete_presenter.rb +53 -0
  63. data/lib/doto/presenters/project/list_presenter.rb +24 -0
  64. data/lib/doto/presenters/project/rename_by_number_presenter.rb +63 -0
  65. data/lib/doto/presenters/project/rename_presenter.rb +57 -0
  66. data/lib/doto/presenters/project/use_by_number_presenter.rb +57 -0
  67. data/lib/doto/presenters/project/use_presenter.rb +56 -0
  68. data/lib/doto/services/color_theme/hydrator_service.rb +42 -0
  69. data/lib/doto/services/configuration/hydrator_service.rb +42 -0
  70. data/lib/doto/services/entry/hydrator_service.rb +33 -0
  71. data/lib/doto/services/entry_group/browse_service.rb +100 -0
  72. data/lib/doto/services/entry_group/counter_service.rb +32 -0
  73. data/lib/doto/services/entry_group/deleter_service.rb +35 -0
  74. data/lib/doto/services/entry_group/editor_service.rb +103 -0
  75. data/lib/doto/services/entry_group/exporter_service.rb +98 -0
  76. data/lib/doto/services/entry_group/hydrator_service.rb +37 -0
  77. data/lib/doto/services/entry_group/importer_service.rb +117 -0
  78. data/lib/doto/services/migration_version/hydrator_service.rb +36 -0
  79. data/lib/doto/services/project/hydrator_service.rb +40 -0
  80. data/lib/doto/services/project/rename_service.rb +70 -0
  81. data/lib/doto/services/stderr_redirector_service.rb +27 -0
  82. data/lib/doto/services/stdout_redirector_service.rb +27 -0
  83. data/lib/doto/services/temp_file/reader_service.rb +33 -0
  84. data/lib/doto/services/temp_file/writer_service.rb +35 -0
  85. data/lib/doto/subcommands/base_subcommand.rb +12 -0
  86. data/lib/doto/subcommands/browse.rb +49 -0
  87. data/lib/doto/subcommands/config.rb +81 -0
  88. data/lib/doto/subcommands/delete.rb +108 -0
  89. data/lib/doto/subcommands/edit.rb +48 -0
  90. data/lib/doto/subcommands/export.rb +62 -0
  91. data/lib/doto/subcommands/import.rb +72 -0
  92. data/lib/doto/subcommands/list.rb +95 -0
  93. data/lib/doto/subcommands/project.rb +146 -0
  94. data/lib/doto/subcommands/theme.rb +131 -0
  95. data/lib/doto/support/ask.rb +44 -0
  96. data/lib/doto/support/color_themable.rb +36 -0
  97. data/lib/doto/support/command_help_colorizeable.rb +34 -0
  98. data/lib/doto/support/command_hookable.rb +71 -0
  99. data/lib/doto/support/command_options/doto_times.rb +48 -0
  100. data/lib/doto/support/command_options/time.rb +84 -0
  101. data/lib/doto/support/command_options/time_mnemonic.rb +108 -0
  102. data/lib/doto/support/command_options/time_mnemonics.rb +16 -0
  103. data/lib/doto/support/descriptable.rb +29 -0
  104. data/lib/doto/support/entry_group_browsable.rb +104 -0
  105. data/lib/doto/support/field_errors.rb +11 -0
  106. data/lib/doto/support/fileable.rb +136 -0
  107. data/lib/doto/support/presentable.rb +11 -0
  108. data/lib/doto/support/project_file_system.rb +118 -0
  109. data/lib/doto/support/short_string.rb +24 -0
  110. data/lib/doto/support/time_comparable.rb +21 -0
  111. data/lib/doto/support/time_formatable.rb +65 -0
  112. data/lib/doto/support/times_sortable.rb +71 -0
  113. data/lib/doto/support/transform_project_name.rb +24 -0
  114. data/lib/doto/support/utils.rb +11 -0
  115. data/lib/doto/validators/color_theme_validator.rb +74 -0
  116. data/lib/doto/validators/description_validator.rb +51 -0
  117. data/lib/doto/validators/entries_validator.rb +77 -0
  118. data/lib/doto/validators/project_name_validator.rb +58 -0
  119. data/lib/doto/validators/time_validator.rb +25 -0
  120. data/lib/doto/validators/version_validator.rb +29 -0
  121. data/lib/doto/version.rb +6 -0
  122. data/lib/doto/views/base_list_view.rb +41 -0
  123. data/lib/doto/views/color_theme/index.rb +62 -0
  124. data/lib/doto/views/color_theme/show.rb +107 -0
  125. data/lib/doto/views/configuration/show.rb +41 -0
  126. data/lib/doto/views/entry_group/edit.rb +121 -0
  127. data/lib/doto/views/entry_group/list.rb +23 -0
  128. data/lib/doto/views/entry_group/shared/no_entries_to_display.rb +53 -0
  129. data/lib/doto/views/entry_group/shared/no_entries_to_display_for_month_of.rb +32 -0
  130. data/lib/doto/views/entry_group/shared/no_entries_to_display_for_week_of.rb +33 -0
  131. data/lib/doto/views/entry_group/shared/no_entries_to_display_for_year_of.rb +33 -0
  132. data/lib/doto/views/entry_group/show.rb +63 -0
  133. data/lib/doto/views/export.rb +82 -0
  134. data/lib/doto/views/import.rb +105 -0
  135. data/lib/doto/views/import_dates.rb +17 -0
  136. data/lib/doto/views/project/create.rb +87 -0
  137. data/lib/doto/views/project/delete.rb +96 -0
  138. data/lib/doto/views/project/delete_by_number.rb +19 -0
  139. data/lib/doto/views/project/list.rb +115 -0
  140. data/lib/doto/views/project/rename.rb +98 -0
  141. data/lib/doto/views/project/rename_by_number.rb +21 -0
  142. data/lib/doto/views/project/use.rb +97 -0
  143. data/lib/doto/views/project/use_by_number.rb +19 -0
  144. data/lib/doto/views/shared/error.rb +17 -0
  145. data/lib/doto/views/shared/info.rb +17 -0
  146. data/lib/doto/views/shared/message.rb +85 -0
  147. data/lib/doto/views/shared/model_errors.rb +32 -0
  148. data/lib/doto/views/shared/success.rb +17 -0
  149. data/lib/doto/views/shared/warning.rb +17 -0
  150. data/lib/doto.rb +33 -0
  151. data/lib/locales/en/active_record.yml +17 -0
  152. data/lib/locales/en/commands.yml +165 -0
  153. data/lib/locales/en/miscellaneous.yml +29 -0
  154. data/lib/locales/en/presenters.yml +19 -0
  155. data/lib/locales/en/services.yml +14 -0
  156. data/lib/locales/en/subcommands.yml +786 -0
  157. data/lib/seed_data/0/.todo +5 -0
  158. data/lib/seed_data/20230613121411/.doto +8 -0
  159. data/lib/seed_data/20230613121411/doto/migration_version.json +3 -0
  160. data/lib/seed_data/20230613121411/doto/themes/cherry.json +79 -0
  161. data/lib/seed_data/20230613121411/doto/themes/christmas.json +79 -0
  162. data/lib/seed_data/20230613121411/doto/themes/default.json +79 -0
  163. data/lib/seed_data/20230613121411/doto/themes/lemon.json +79 -0
  164. data/lib/seed_data/20230613121411/doto/themes/light.json +79 -0
  165. data/lib/seed_data/20230613121411/doto/themes/matrix.json +79 -0
  166. data/lib/seed_data/20230613121411/doto/themes/whiteout.json +79 -0
  167. data/lib/seed_data/20240210161248/.doto +9 -0
  168. data/lib/seed_data/20240210161248/doto/current_project.json +4 -0
  169. data/lib/seed_data/20240210161248/doto/migration_version.json +3 -0
  170. data/lib/seed_data/20240210161248/doto/projects/default/project.json +5 -0
  171. data/lib/seed_data/20240210161248/doto/themes/cherry.json +79 -0
  172. data/lib/seed_data/20240210161248/doto/themes/christmas.json +79 -0
  173. data/lib/seed_data/20240210161248/doto/themes/default.json +79 -0
  174. data/lib/seed_data/20240210161248/doto/themes/lemon.json +79 -0
  175. data/lib/seed_data/20240210161248/doto/themes/light.json +79 -0
  176. data/lib/seed_data/20240210161248/doto/themes/matrix.json +79 -0
  177. data/lib/seed_data/20240210161248/doto/themes/whiteout.json +79 -0
  178. data/sig/dsu.rbs +4 -0
  179. metadata +406 -0
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+ require_relative '../../models/entry_group'
5
+ require_relative '../../models/project'
6
+
7
+ module Doto
8
+ module Services
9
+ module EntryGroup
10
+ # Expects a hash having the following format:
11
+ # {
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
+ # }
22
+ # }
23
+ class ImporterService
24
+ include Support::Fileable
25
+
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)
29
+
30
+ raise_if_more_than_one_project(import_projects)
31
+
32
+ @import_projects = import_projects
33
+ @options = options
34
+ end
35
+
36
+ def call
37
+ import!
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :import_projects, :options
43
+
44
+ def import!
45
+ project_entry_groups.each_pair do |entry_group_date, entry_descriptions|
46
+ entry_group_for(entry_group_date).tap do |entry_group|
47
+ entry_descriptions.each do |entry_description|
48
+ add_entry_group_entry_if(entry_group: entry_group, entry_description: entry_description)
49
+ end
50
+
51
+ import_messages[entry_group.time_yyyy_mm_dd] = []
52
+
53
+ unless entry_group.save
54
+ entry_group.errors.full_messages.each do |error|
55
+ import_messages[entry_group.time_yyyy_mm_dd] << error
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ import_messages
62
+ end
63
+
64
+ def entry_group_for(entry_group_date)
65
+ time = Time.parse(entry_group_date).in_time_zone
66
+ if merge?
67
+ Models::EntryGroup.find_or_initialize(time: time)
68
+ else
69
+ Models::EntryGroup.new(time: time, options: options)
70
+ end
71
+ end
72
+
73
+ def add_entry_group_entry_if(entry_group:, entry_description:)
74
+ entry = Models::Entry.new(description: entry_description)
75
+ return entry_group.entries << entry if replace?
76
+ return if entry_group.entries.include?(entry)
77
+
78
+ entry_group.entries << entry
79
+ end
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
+
89
+ def merge?
90
+ options.fetch(:merge, true)
91
+ end
92
+
93
+ def replace?
94
+ !merge?
95
+ end
96
+
97
+ def override_project?
98
+ options.fetch(:override, false)
99
+ end
100
+
101
+ def import_messages
102
+ @import_messages ||= {}
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
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Services
5
+ module MigrationVersion
6
+ class HydratorService
7
+ def initialize(migration_version_hash:, options: {})
8
+ raise ArgumentError, 'migration_version_hash is nil' if migration_version_hash.nil?
9
+
10
+ unless migration_version_hash.is_a?(Hash)
11
+ raise ArgumentError,
12
+ "migration_version_hash is the wrong object type: \"#{migration_version_hash}\""
13
+ end
14
+ raise ArgumentError, 'options is nil' if options.nil?
15
+ raise ArgumentError, "options is the wrong object type:\"#{options}\"" unless options.is_a?(Hash)
16
+
17
+ @migration_version_hash = migration_version_hash.dup
18
+ @options = options || {}
19
+ end
20
+
21
+ def call
22
+ hydrate
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :migration_version_hash, :options
28
+
29
+ def hydrate
30
+ migration_version_hash[:version] = migration_version_hash[:version].to_i
31
+ migration_version_hash
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/color_theme'
4
+
5
+ module Doto
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 Doto
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
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Services
5
+ # This service captures $stderr, resirects it to a StringIO object,
6
+ # and returns the string value.
7
+ # https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
8
+ module StderrRedirectorService
9
+ class << self
10
+ def call
11
+ raise ArgumentError, 'no block was provided' unless block_given?
12
+
13
+ # The output stream must be an IO-like object. In this case we capture it in
14
+ # an in-memory IO object so we can return the string value. Any IO object can
15
+ # be used here.
16
+ string_io = StringIO.new
17
+ original_stderr, $stderr = $stderr, string_io # rubocop:disable Style/ParallelAssignment
18
+ yield
19
+ string_io.string
20
+ ensure
21
+ # Restore the original $stderr.
22
+ $stderr = original_stderr
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Services
5
+ # This service captures $stdout, resirects it to a StringIO object,
6
+ # and returns the string value.
7
+ # https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
8
+ module StdoutRedirectorService
9
+ class << self
10
+ def call
11
+ raise ArgumentError, 'no block was provided' unless block_given?
12
+
13
+ # The output stream must be an IO-like object. In this case we capture it in
14
+ # an in-memory IO object so we can return the string value. Any IO object can
15
+ # be used here.
16
+ string_io = StringIO.new
17
+ original_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
18
+ yield
19
+ string_io.string
20
+ ensure
21
+ # Restore the original $stdout.
22
+ $stdout = original_stdout
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Services
5
+ module TempFile
6
+ class ReaderService
7
+ def initialize(tmp_file_path:, options: {})
8
+ raise ArgumentError, 'tmp_file_path is nil' if tmp_file_path.nil?
9
+ raise ArgumentError, 'tmp_file_path is the wrong object type' unless tmp_file_path.is_a?(String)
10
+ raise ArgumentError, 'tmp_file_path is empty' if tmp_file_path.empty?
11
+ raise ArgumentError, 'tmp_file_path does not exist' unless File.exist?(tmp_file_path)
12
+ raise ArgumentError, 'options is nil' if options.nil?
13
+ raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
14
+
15
+ @tmp_file_path = tmp_file_path
16
+ @options = options || {}
17
+ end
18
+
19
+ def call
20
+ raise ArgumentError, 'no block given' unless block_given?
21
+
22
+ File.foreach(tmp_file_path) do |line|
23
+ yield line.strip
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :tmp_file_path, :options
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+
5
+ module Doto
6
+ module Services
7
+ module TempFile
8
+ class WriterService
9
+ def initialize(tmp_file_content:, options: {})
10
+ raise ArgumentError, 'tmp_file_content is nil' if tmp_file_content.nil?
11
+ raise ArgumentError, 'tmp_file_content is the wrong object type' unless tmp_file_content.is_a?(String)
12
+ raise ArgumentError, 'options is nil' if options.nil?
13
+ raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
14
+
15
+ @tmp_file_content = tmp_file_content
16
+ @options = options || {}
17
+ end
18
+
19
+ def call
20
+ raise ArgumentError, 'no block given' unless block_given?
21
+
22
+ Tempfile.new('doto').tap do |file|
23
+ file.write("#{tmp_file_content}\n")
24
+ file.close
25
+ yield file.path
26
+ end.unlink
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :tmp_file_content, :options
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_cli'
4
+ require_relative '../support/ask'
5
+
6
+ module Doto
7
+ module Subcommands
8
+ class BaseSubcommand < Doto::BaseCLI
9
+ include Support::Ask
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ require_relative '../services/stdout_redirector_service'
6
+ require_relative '../support/command_options/doto_times'
7
+ require_relative '../support/command_options/time_mnemonic'
8
+ require_relative '../support/entry_group_browsable'
9
+ require_relative '../support/time_formatable'
10
+ require_relative '../views/entry_group/shared/no_entries_to_display'
11
+ require_relative '../views/shared/error'
12
+ require_relative 'base_subcommand'
13
+
14
+ module Doto
15
+ module Subcommands
16
+ class Browse < BaseSubcommand
17
+ include Support::EntryGroupBrowsable
18
+ include Support::CommandOptions::TimeMnemonic
19
+ include Support::TimeFormatable
20
+
21
+ # TODO: I18n.
22
+ map %w[w] => :week
23
+ map %w[m] => :month
24
+ map %w[y] => :year
25
+
26
+ class_option :pager, default: true, type: :boolean, hide: true, aliases: '-p'
27
+ class_option :include_all, default: nil, type: :boolean, aliases: '-a',
28
+ desc: I18n.t('options.include_all')
29
+
30
+ desc I18n.t('subcommands.browse.week.desc'), I18n.t('subcommands.browse.week.usage')
31
+ long_desc I18n.t('subcommands.browse.week.long_desc')
32
+ def week
33
+ browse_entry_groups time: Time.now, options: options.merge({ browse: :week })
34
+ end
35
+
36
+ desc I18n.t('subcommands.browse.month.desc'), I18n.t('subcommands.browse.month.usage')
37
+ long_desc I18n.t('subcommands.browse.month.long_desc')
38
+ def month
39
+ browse_entry_groups time: Time.now, options: options.merge({ browse: :month })
40
+ end
41
+
42
+ desc I18n.t('subcommands.browse.year.desc'), I18n.t('subcommands.browse.year.usage')
43
+ long_desc I18n.t('subcommands.browse.year.long_desc')
44
+ def year
45
+ browse_entry_groups time: Time.now, options: options.merge({ browse: :year })
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/configuration'
4
+ require_relative '../views/configuration/show'
5
+ require_relative '../views/shared/model_errors'
6
+ require_relative '../views/shared/success'
7
+ require_relative '../views/shared/warning'
8
+ require_relative 'base_subcommand'
9
+
10
+ module Doto
11
+ module Subcommands
12
+ class Config < BaseSubcommand
13
+ default_command :help
14
+
15
+ class << self
16
+ def exit_on_failure?
17
+ false
18
+ end
19
+ end
20
+
21
+ desc I18n.t('subcommands.config.info.desc'), I18n.t('subcommands.config.info.usage')
22
+ long_desc I18n.t('subcommands.config.info.long_desc')
23
+ def info
24
+ configuration = Models::Configuration.new
25
+ Views::Configuration::Show.new(config: configuration).call
26
+ end
27
+
28
+ if Doto.env.development?
29
+ desc I18n.t('subcommands.config.init.desc'), I18n.t('subcommands.config.init.usage')
30
+ long_desc I18n.t('subcommands.config.init.long_desc', home_folder: Doto::Support::Fileable.root_folder)
31
+ def init
32
+ exit 1 if configuration_errors_or_wanings?
33
+
34
+ Models::Configuration.default.tap do |configuration|
35
+ configuration.save!
36
+ messages = [I18n.t('messages.configuration_file.created',
37
+ configuration_file: Models::Configuration.config_file)]
38
+ Views::Shared::Success.new(messages: messages).render
39
+ Views::Configuration::Show.new(config: configuration).render
40
+ end
41
+ end
42
+
43
+ desc I18n.t('subcommands.config.delete.desc'), I18n.t('subcommands.config.delete.usage')
44
+ long_desc I18n.t('subcommands.config.delete.long_desc')
45
+ def delete
46
+ unless Models::Configuration.exist?
47
+ messages = [I18n.t('messages.configuration_file.does_not_exist',
48
+ configuration_file: Models::Configuration.config_file)]
49
+ Views::Shared::Warning.new(messages: messages).render
50
+ exit 1
51
+ end
52
+ Models::Configuration.delete!
53
+ messages = [I18n.t('messages.configuration_file.deleted',
54
+ configuration_file: Models::Configuration.config_file)]
55
+ Views::Shared::Success.new(messages: messages).render
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def configuration_errors_or_wanings?
62
+ if Models::Configuration.exist?
63
+ messages = [I18n.t('messages.configuration_file.already_exists',
64
+ configuration_file: Models::Configuration.config_file)]
65
+ Views::Shared::Warning.new(messages: messages).render
66
+ elsif !Dir.exist?(Models::Configuration.config_folder)
67
+ messages = [I18n.t('messages.configuration_file.destination_folder_does_not_exist',
68
+ configuration_file: Models::Configuration.config_file)]
69
+ Views::Shared::Error.new(messages: messages).render
70
+ else
71
+ configuration = Models::Configuration.default
72
+ return false if configuration.valid?
73
+
74
+ Views::Shared::ModelErrors.new(model: configuration).render
75
+ end
76
+
77
+ true
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../services/entry_group/counter_service'
4
+ require_relative '../services/entry_group/deleter_service'
5
+ require_relative '../support/command_options/doto_times'
6
+ require_relative '../support/command_options/time_mnemonic'
7
+ require_relative '../support/time_formatable'
8
+ require_relative '../views/entry_group/shared/no_entries_to_display'
9
+ require_relative '../views/shared/error'
10
+ require_relative 'base_subcommand'
11
+
12
+ module Doto
13
+ module Subcommands
14
+ class Delete < BaseSubcommand
15
+ include Support::CommandOptions::TimeMnemonic
16
+ include Support::TimeFormatable
17
+
18
+ # TODO: I18n.
19
+ map %w[d] => :date
20
+ map %w[dd] => :dates
21
+ map %w[n] => :today
22
+ map %w[t] => :tomorrow
23
+ map %w[y] => :yesterday
24
+
25
+ desc I18n.t('subcommands.delete.today.desc'), I18n.t('subcommands.delete.today.usage')
26
+ long_desc I18n.t('subcommands.delete.today.long_desc')
27
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
28
+ def today
29
+ delete_entry_groups_if times: [Time.now], options: options
30
+ end
31
+
32
+ desc I18n.t('subcommands.delete.tomorrow.desc'), I18n.t('subcommands.delete.tomorrow.usage')
33
+ long_desc I18n.t('subcommands.delete.tomorrow.long_desc')
34
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
35
+ def tomorrow
36
+ delete_entry_groups_if times: [Time.now.tomorrow], options: options
37
+ end
38
+
39
+ desc I18n.t('subcommands.delete.yesterday.desc'), I18n.t('subcommands.delete.yesterday.usage')
40
+ long_desc I18n.t('subcommands.delete.yesterday.long_desc')
41
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
42
+ def yesterday
43
+ delete_entry_groups_if times: [Time.now.yesterday], options: options
44
+ end
45
+
46
+ desc I18n.t('subcommands.delete.date.desc'), I18n.t('subcommands.delete.date.usage')
47
+ long_desc I18n.t('subcommands.delete.date.long_desc',
48
+ date_option_description: date_option_description,
49
+ mnemonic_option_description: mnemonic_option_description)
50
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
51
+ def date(date_or_mnemonic)
52
+ time = if time_mnemonic?(date_or_mnemonic)
53
+ time_from_mnemonic(command_option: date_or_mnemonic)
54
+ else
55
+ Time.parse(date_or_mnemonic)
56
+ end
57
+ delete_entry_groups_if times: [time], options: options
58
+ rescue ArgumentError => e
59
+ Views::Shared::Error.new(messages: e.message).render
60
+ end
61
+
62
+ desc I18n.t('subcommands.delete.dates.desc'), I18n.t('subcommands.delete.dates.usage')
63
+ long_desc I18n.t('subcommands.delete.dates.long_desc',
64
+ date_option_description: date_option_description,
65
+ mnemonic_option_description: mnemonic_option_description)
66
+ option :from, type: :string, required: true, aliases: '-f', banner: 'DATE|MNEMONIC'
67
+ option :to, type: :string, required: true, aliases: '-t', banner: 'DATE|MNEMONIC'
68
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
69
+ def dates
70
+ options = configuration.to_h.merge(self.options).with_indifferent_access
71
+ times, errors = Support::CommandOptions::DotoTimes.doto_times_for(from_option: options[:from], to_option: options[:to]) # rubocop:disable Layout/LineLength
72
+ if errors.any?
73
+ Views::Shared::Error.new(messages: errors).render
74
+ return
75
+ end
76
+
77
+ times = times_sort(times: times, entries_display_order: options[:entries_display_order])
78
+ delete_entry_groups_if times: times, options: options
79
+ rescue ArgumentError => e
80
+ Views::Shared::Error.new(messages: e.message).render
81
+ end
82
+
83
+ private
84
+
85
+ def delete_entry_groups_if(times:, options:)
86
+ prompt_string = I18n.t('subcommands.delete.prompts.are_you_sure',
87
+ dates: yyyy_mm_dd_or_through_for(times: times), count: total_entry_groups_for(times: times))
88
+ prompt = color_theme.prompt_with_options(prompt: prompt_string, options: %w[y N])
89
+ if yes?(prompt, options: options)
90
+ deleted_count = delete_entry_groups_for(times: times)
91
+ message = I18n.t('subcommands.delete.messages.deleted', count: deleted_count)
92
+ Views::Shared::Success.new(messages: message).render
93
+ else
94
+ message = I18n.t('subcommands.delete.messages.cancelled')
95
+ Views::Shared::Info.new(messages: message).render
96
+ end
97
+ end
98
+
99
+ def delete_entry_groups_for(times:)
100
+ Services::EntryGroup::DeleterService.new(times: times).call
101
+ end
102
+
103
+ def total_entry_groups_for(times:)
104
+ Services::EntryGroup::CounterService.new(times: times).call
105
+ end
106
+ end
107
+ end
108
+ end