doto 0.0.1.pre.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
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