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,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_subcommand'
4
+ require_relative '../models/entry_group'
5
+ require_relative '../views/entry_group/show'
6
+
7
+ module Doto
8
+ module Subcommands
9
+ class Edit < BaseSubcommand
10
+ # TODO: I18n.
11
+ map %w[d] => :date
12
+ map %w[n] => :today
13
+ map %w[t] => :tomorrow
14
+ map %w[y] => :yesterday
15
+
16
+ desc I18n.t('subcommands.edit.date.desc'), I18n.t('subcommands.edit.date.usage')
17
+ long_desc I18n.t('subcommands.edit.date.long_desc', date_option_description: date_option_description)
18
+ def date(date)
19
+ entry_group = Models::EntryGroup.edit(time: Time.parse(date))
20
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
21
+ rescue ArgumentError => e
22
+ puts apply_theme(I18n.t('errors.error', message: e.message), theme_color: color_theme.error)
23
+ exit 1
24
+ end
25
+
26
+ desc I18n.t('subcommands.edit.today.desc'), I18n.t('subcommands.edit.today.usage')
27
+ long_desc I18n.t('subcommands.edit.today.long_desc')
28
+ def today
29
+ entry_group = Models::EntryGroup.edit(time: Time.now)
30
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
31
+ end
32
+
33
+ desc I18n.t('subcommands.edit.tomorrow.desc'), I18n.t('subcommands.edit.tomorrow.usage')
34
+ long_desc I18n.t('subcommands.edit.tomorrow.long_desc')
35
+ def tomorrow
36
+ entry_group = Models::EntryGroup.edit(time: Time.now.tomorrow)
37
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
38
+ end
39
+
40
+ desc I18n.t('subcommands.edit.yesterday.desc'), I18n.t('subcommands.edit.yesterday.usage')
41
+ long_desc I18n.t('subcommands.edit.yesterday.long_desc')
42
+ def yesterday
43
+ entry_group = Models::EntryGroup.edit(time: Time.now.yesterday)
44
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../presenters/export/all_presenter'
4
+ require_relative '../presenters/export/dates_presenter'
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/export'
9
+ require_relative '../views/shared/error'
10
+ require_relative 'base_subcommand'
11
+
12
+ module Doto
13
+ module Subcommands
14
+ class Export < BaseSubcommand
15
+ include Support::CommandOptions::TimeMnemonic
16
+ include Support::TimeFormatable
17
+
18
+ # TODO: I18n.
19
+ map %w[a] => :all
20
+ map %w[dd] => :dates
21
+
22
+ desc I18n.t('subcommands.export.all.desc'), I18n.t('subcommands.export.all.usage')
23
+ long_desc I18n.t('subcommands.export.all.long_desc')
24
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
25
+ def all
26
+ options = configuration.to_h.merge(self.options).with_indifferent_access
27
+ Views::Export.new(presenter: all_presenter(options: options), options: options).render
28
+ end
29
+
30
+ desc I18n.t('subcommands.export.dates.desc'), I18n.t('subcommands.export.dates.usage')
31
+ long_desc I18n.t('subcommands.export.dates.long_desc',
32
+ date_option_description: date_option_description,
33
+ mnemonic_option_description: mnemonic_option_description)
34
+ option :from, type: :string, required: true, aliases: '-f', banner: 'DATE|MNEMONIC'
35
+ option :to, type: :string, required: true, aliases: '-t', banner: 'DATE|MNEMONIC'
36
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
37
+ def dates
38
+ options = configuration.to_h.merge(self.options).with_indifferent_access
39
+ times, errors = Support::CommandOptions::DotoTimes.doto_times_for(from_option: options[:from], to_option: options[:to]) # rubocop:disable Layout/LineLength
40
+ if errors.any?
41
+ Views::Shared::Error.new(messages: errors).render
42
+ return
43
+ end
44
+
45
+ Views::Export.new(presenter:
46
+ dates_presenter_for(from: times.min, to: times.max, options: options), options: options).render
47
+ rescue ArgumentError => e
48
+ Views::Shared::Error.new(messages: e.message).render
49
+ end
50
+
51
+ private
52
+
53
+ def all_presenter(options:)
54
+ Presenters::Export::AllPresenter.new(options: options)
55
+ end
56
+
57
+ def dates_presenter_for(from:, to:, options:)
58
+ Presenters::Export::DatesPresenter.new(from: from, to: to, options: options)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../presenters/import/all_presenter'
4
+ require_relative '../presenters/import/dates_presenter'
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/import'
9
+ require_relative '../views/import_dates'
10
+ require_relative '../views/shared/error'
11
+ require_relative 'base_subcommand'
12
+
13
+ module Doto
14
+ module Subcommands
15
+ class Import < BaseSubcommand
16
+ include Support::CommandOptions::TimeMnemonic
17
+ include Support::TimeFormatable
18
+
19
+ # TODO: I18n.
20
+ map %w[a] => :all
21
+ map %w[dd] => :dates
22
+
23
+ desc I18n.t('subcommands.import.all.desc'), I18n.t('subcommands.import.all.usage')
24
+ long_desc I18n.t('subcommands.import.all.long_desc')
25
+ option :import_file, type: :string, required: true, aliases: '-i', banner: 'IMPORT_CVS_FILE'
26
+ option :merge, type: :boolean, default: true, aliases: '-m'
27
+ option :override, type: :boolean, default: false, aliases: '-o'
28
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
29
+ def all
30
+ options = configuration.to_h.merge(self.options).with_indifferent_access
31
+ Views::Import.new(presenter: all_presenter(import_file_path: options[:import_file],
32
+ options: options), options: options).render
33
+ end
34
+
35
+ desc I18n.t('subcommands.import.dates.desc'), I18n.t('subcommands.import.dates.usage')
36
+ long_desc I18n.t('subcommands.import.dates.long_desc',
37
+ date_option_description: date_option_description,
38
+ mnemonic_option_description: mnemonic_option_description)
39
+ option :from, type: :string, required: true, aliases: '-f', banner: 'DATE|MNEMONIC'
40
+ option :to, type: :string, required: true, aliases: '-t', banner: 'DATE|MNEMONIC'
41
+ option :import_file, type: :string, required: true, aliases: '-i', banner: 'IMPORT_CVS_FILE'
42
+ option :merge, type: :boolean, default: true, aliases: '-m'
43
+ option :override, type: :boolean, default: false, aliases: '-o'
44
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
45
+ def dates
46
+ options = configuration.to_h.merge(self.options).with_indifferent_access
47
+ times, errors = Support::CommandOptions::DotoTimes.doto_times_for(from_option: options[:from], to_option: options[:to]) # rubocop:disable Layout/LineLength
48
+ if errors.any?
49
+ Views::Shared::Error.new(messages: errors).render
50
+ return
51
+ end
52
+
53
+ Views::ImportDates.new(presenter: dates_presenter_for(from: times.min,
54
+ to: times.max,
55
+ import_file_path: options[:import_file],
56
+ options: options), options: options).render
57
+ rescue ArgumentError => e
58
+ Views::Shared::Error.new(messages: e.message).render
59
+ end
60
+
61
+ private
62
+
63
+ def all_presenter(import_file_path:, options:)
64
+ Presenters::Import::AllPresenter.new(import_file_path: import_file_path, options: options)
65
+ end
66
+
67
+ def dates_presenter_for(from:, to:, import_file_path:, options:)
68
+ Presenters::Import::DatesPresenter.new(from: from, to: to, import_file_path: import_file_path, options: options)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../presenters/entry_group/list/date_presenter'
4
+ require_relative '../presenters/entry_group/list/dates_presenter'
5
+ require_relative '../services/entry_group/counter_service'
6
+ require_relative '../support/command_options/doto_times'
7
+ require_relative '../support/command_options/time_mnemonic'
8
+ require_relative '../support/time_formatable'
9
+ require_relative '../views/entry_group/list'
10
+ require_relative '../views/shared/error'
11
+ require_relative 'base_subcommand'
12
+
13
+ module Doto
14
+ module Subcommands
15
+ class List < BaseSubcommand
16
+ include Support::CommandOptions::TimeMnemonic
17
+ include Support::TimeFormatable
18
+
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.list.today.desc'), I18n.t('subcommands.list.today.usage')
26
+ long_desc I18n.t('subcommands.list.today.long_desc')
27
+ def today
28
+ time = Time.now
29
+ times = sorted_doto_times_for(times: [time.yesterday, time])
30
+ presenter = Presenters::EntryGroup::List::DatePresenter.new(times: times, options: options)
31
+ Views::EntryGroup::List.new(presenter: presenter).render
32
+ end
33
+
34
+ desc I18n.t('subcommands.list.tomorrow.desc'), I18n.t('subcommands.list.tomorrow.usage')
35
+ long_desc I18n.t('subcommands.list.tomorrow.long_desc')
36
+ def tomorrow
37
+ time = Time.now
38
+ times = sorted_doto_times_for(times: [time, time.tomorrow])
39
+ presenter = Presenters::EntryGroup::List::DatePresenter.new(times: times, options: options)
40
+ Views::EntryGroup::List.new(presenter: presenter).render
41
+ end
42
+
43
+ desc I18n.t('subcommands.list.yesterday.desc'), I18n.t('subcommands.list.yesterday.usage')
44
+ long_desc I18n.t('subcommands.list.yesterday.long_desc')
45
+ def yesterday
46
+ time = Time.now
47
+ times = sorted_doto_times_for(times: [time.yesterday, time.yesterday.yesterday])
48
+ presenter = Presenters::EntryGroup::List::DatePresenter.new(times: times, options: options)
49
+ Views::EntryGroup::List.new(presenter: presenter).render
50
+ end
51
+
52
+ desc I18n.t('subcommands.list.date.desc'), I18n.t('subcommands.list.date.usage')
53
+ long_desc I18n.t('subcommands.list.date.long_desc',
54
+ date_option_description: date_option_description,
55
+ mnemonic_option_description: mnemonic_option_description)
56
+ def date(date_or_mnemonic)
57
+ time = if time_mnemonic?(date_or_mnemonic)
58
+ time_from_mnemonic(command_option: date_or_mnemonic)
59
+ else
60
+ Time.parse(date_or_mnemonic)
61
+ end
62
+ times = sorted_doto_times_for(times: [time, time.yesterday])
63
+ presenter = Presenters::EntryGroup::List::DatePresenter.new(times: times, options: options)
64
+ Views::EntryGroup::List.new(presenter: presenter).render
65
+ rescue ArgumentError => e
66
+ Views::Shared::Error.new(messages: e.message).render
67
+ end
68
+
69
+ desc I18n.t('subcommands.list.dates.desc'), I18n.t('subcommands.list.dates.usage')
70
+ long_desc I18n.t('subcommands.list.dates.long_desc',
71
+ date_option_description: date_option_description,
72
+ mnemonic_option_description: mnemonic_option_description)
73
+ option :from, type: :string, required: true, aliases: '-f', banner: 'DATE|MNEMONIC'
74
+ option :to, type: :string, required: true, aliases: '-t', banner: 'DATE|MNEMONIC'
75
+ option :include_all, default: nil, type: :boolean, aliases: '-a',
76
+ desc: I18n.t('options.include_all')
77
+ def dates
78
+ options = configuration.to_h.merge(self.options).with_indifferent_access
79
+ times, errors = Support::CommandOptions::DotoTimes.doto_times_for(from_option: options[:from], to_option: options[:to]) # rubocop:disable Layout/LineLength
80
+ if errors.any?
81
+ Views::Shared::Error.new(messages: errors).render
82
+ return
83
+ end
84
+
85
+ # NOTE: special sort here, unlike the other commands where rules for
86
+ # displaying TODO entries are applied; this is more of a list command.
87
+ times = times_sort(times: times, entries_display_order: options[:entries_display_order])
88
+ presenter = Presenters::EntryGroup::List::DatesPresenter.new(times: times, options: options)
89
+ Views::EntryGroup::List.new(presenter: presenter).render
90
+ rescue ArgumentError => e
91
+ Views::Shared::Error.new(messages: e.message).render
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,146 @@
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 Doto
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
+ def create(project_name = nil, description = nil)
27
+ project_name = project_name.to_s.strip
28
+ description = description.to_s.strip
29
+ if project_name.blank?
30
+ return Views::Shared::Error.new(
31
+ messages: I18n.t('subcommands.project.messages.project_name_blank')
32
+ ).render
33
+ end
34
+
35
+ options = configuration.to_h.merge(self.options).with_indifferent_access
36
+ presenter = Presenters::Project::CreatePresenter.new(project_name: project_name,
37
+ description: description, options: options)
38
+ Views::Project::Create.new(presenter: presenter, options: options).render
39
+ end
40
+
41
+ desc I18n.t('subcommands.project.delete.desc'), I18n.t('subcommands.project.delete.usage')
42
+ long_desc I18n.t('subcommands.project.delete.long_desc')
43
+ def delete(project_name_or_number = nil)
44
+ options = configuration.to_h.merge(self.options).with_indifferent_access
45
+ presenter = delete_presenter_for(project_name_or_number, options: options)
46
+ delete_view_for(project_name_or_number, presenter: presenter, options: options).render
47
+ end
48
+
49
+ desc I18n.t('subcommands.project.list.desc'), I18n.t('subcommands.project.list.usage')
50
+ long_desc I18n.t('subcommands.project.list.long_desc')
51
+ def list
52
+ options = configuration.to_h.merge(self.options).with_indifferent_access
53
+ presenter = Presenters::Project::ListPresenter.new(options: options)
54
+ Views::Project::List.new(presenter: presenter, options: options).render
55
+ end
56
+
57
+ desc I18n.t('subcommands.project.rename.desc'), I18n.t('subcommands.project.rename.usage')
58
+ long_desc I18n.t('subcommands.project.rename.long_desc')
59
+ def rename(project_name_or_number = nil, new_project_name = nil, new_project_description = nil)
60
+ project_name_or_number = project_name_or_number.to_s.strip
61
+ new_project_name = new_project_name&.to_s&.strip
62
+ new_project_description = new_project_description&.to_s&.strip
63
+
64
+ if new_project_name.blank?
65
+ return Views::Shared::Error.new(
66
+ messages: I18n.t('subcommands.project.messages.new_project_name_blank')
67
+ ).render
68
+ end
69
+
70
+ options = configuration.to_h.merge(self.options).with_indifferent_access
71
+ presenter = rename_presenter_for(project_name_or_number, new_project_name: new_project_name,
72
+ new_project_description: new_project_description, options: options)
73
+ rename_view_for(project_name_or_number, presenter: presenter, options: options).render
74
+ end
75
+
76
+ desc I18n.t('subcommands.project.use.desc'),
77
+ I18n.t('subcommands.project.use.usage')
78
+ long_desc I18n.t('subcommands.project.use.long_desc')
79
+ option :default, type: :boolean, required: false, default: false, aliases: '-d', banner: 'DEFAULT_PROJECT'
80
+ def use(project_name_or_number = nil)
81
+ options = configuration.to_h.merge(self.options).with_indifferent_access
82
+ presenter = use_presenter_for(project_name_or_number, options: options)
83
+ use_view_for(project_name_or_number, presenter: presenter, options: options).render
84
+ end
85
+
86
+ private
87
+
88
+ def delete_view_for(project_name, presenter:, options:)
89
+ if project_number?(project_name)
90
+ Views::Project::DeleteByNumber.new(presenter: presenter, options: options)
91
+ else
92
+ Views::Project::Delete.new(presenter: presenter, options: options)
93
+ end
94
+ end
95
+
96
+ def delete_presenter_for(project_name, options:)
97
+ if project_number?(project_name)
98
+ Presenters::Project::DeleteByNumberPresenter.new(project_number: project_name.to_i, options: options)
99
+ else
100
+ project_name = Models::Project.default_project_name if project_name.blank?
101
+ Presenters::Project::DeletePresenter.new(project_name: project_name, options: options)
102
+ end
103
+ end
104
+
105
+ def rename_view_for(project_name, presenter:, options:)
106
+ if project_number?(project_name)
107
+ Views::Project::RenameByNumber.new(presenter: presenter, options: options)
108
+ else
109
+ Views::Project::Rename.new(presenter: presenter, options: options)
110
+ end
111
+ end
112
+
113
+ def rename_presenter_for(project_name, new_project_name:, new_project_description:, options:)
114
+ if project_number?(project_name)
115
+ Presenters::Project::RenameByNumberPresenter.new(project_number: project_name.to_i,
116
+ new_project_name: new_project_name, new_project_description: new_project_description, options: options)
117
+ else
118
+ project_name = Models::Project.default_project_name if project_name.blank?
119
+ Presenters::Project::RenamePresenter.new(project_name: project_name,
120
+ new_project_name: new_project_name, new_project_description: new_project_description, options: options)
121
+ end
122
+ end
123
+
124
+ def use_view_for(project_name, presenter:, options:)
125
+ if project_number?(project_name)
126
+ Views::Project::UseByNumber.new(presenter: presenter, options: options)
127
+ else
128
+ Views::Project::Use.new(presenter: presenter, options: options)
129
+ end
130
+ end
131
+
132
+ def use_presenter_for(project_name, options:)
133
+ if project_number?(project_name)
134
+ Presenters::Project::UseByNumberPresenter.new(project_number: project_name.to_i, options: options)
135
+ else
136
+ project_name = Models::Project.default_project_name if project_name.blank?
137
+ Presenters::Project::UsePresenter.new(project_name: project_name, options: options)
138
+ end
139
+ end
140
+
141
+ def project_number?(project_name)
142
+ /^[+-]?\d+(\.\d+)?$/.match?(project_name.to_s)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../env'
4
+ require_relative '../support/fileable'
5
+ require_relative '../views/color_theme/index'
6
+ require_relative '../views/color_theme/show'
7
+ require_relative '../views/shared/error'
8
+ require_relative '../views/shared/info'
9
+ require_relative 'base_subcommand'
10
+
11
+ module Doto
12
+ module Subcommands
13
+ class Theme < BaseSubcommand
14
+ # TODO: I18n.
15
+ map %w[c] => :create if Doto.env.local?
16
+ map %w[d] => :delete if Doto.env.local?
17
+ map %w[l] => :list
18
+ map %w[s] => :show
19
+ map %w[u] => :use
20
+
21
+ if Doto.env.local?
22
+ desc I18n.t('subcommands.theme.create.desc'), I18n.t('subcommands.theme.create.usage')
23
+ long_desc I18n.t('subcommands.theme.create.long_desc', themes_folder: Support::Fileable.themes_folder)
24
+ option :description, type: :string, aliases: '-d', banner: 'DESCRIPTION'
25
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
26
+ def create(theme_name)
27
+ if Models::ColorTheme.exist?(theme_name: theme_name)
28
+ message = I18n.t('subcommands.theme.create.errors.already_exists', theme_name: theme_name)
29
+ Views::Shared::Error.new(messages: message).render
30
+ return false
31
+ end
32
+ prompt_string = I18n.t('subcommands.theme.create.prompts.create_theme', theme_name: theme_name)
33
+ prompt = color_theme.prompt_with_options(prompt: prompt_string, options: %w[y N])
34
+ if yes?(prompt, options: options)
35
+ theme_hash = Models::ColorTheme::DEFAULT_THEME.dup
36
+ theme_hash[:description] = options[:description] || I18n.t('subcommands.theme.generic.color_theme',
37
+ theme_name: theme_name.capitalize)
38
+ Models::ColorTheme.new(theme_name: theme_name, theme_hash: theme_hash).save!
39
+ message = I18n.t('subcommands.theme.create.messages.created', theme_name: theme_name)
40
+ Views::Shared::Info.new(messages: "\n#{message}").render
41
+ true
42
+ else
43
+ message = I18n.t('subcommands.theme.create.messages.cancelled')
44
+ Views::Shared::Info.new(messages: "\n#{message}").render
45
+ false
46
+ end
47
+ end
48
+
49
+ desc I18n.t('subcommands.theme.delete.desc'), I18n.t('subcommands.theme.delete.usage')
50
+ long_desc I18n.t('subcommands.theme.delete.long_desc', themes_folder: Support::Fileable.themes_folder)
51
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
52
+ def delete(theme_name) # rubocop:disable Metrics/MethodLength
53
+ display_doto_header
54
+
55
+ if theme_name == Models::ColorTheme::DEFAULT_THEME_NAME
56
+ message = I18n.t('subcommands.theme.delete.errors.cannot_delete', theme_name: theme_name)
57
+ Views::Shared::Error.new(messages: message).render
58
+ return
59
+ end
60
+
61
+ unless Models::ColorTheme.exist?(theme_name: theme_name)
62
+ message = I18n.t('subcommands.theme.generic.errors.does_not_exist', theme_name: theme_name)
63
+ Views::Shared::Error.new(messages: message).render
64
+ return
65
+ end
66
+
67
+ prompt_string = I18n.t('subcommands.theme.delete.prompts.delete_theme', theme_name: theme_name)
68
+ prompt = color_theme.prompt_with_options(prompt: prompt_string, options: %w[y N])
69
+ message = if yes?(prompt, options: options)
70
+ Models::ColorTheme.delete!(theme_name: theme_name)
71
+ change_theme
72
+ I18n.t('subcommands.theme.delete.messages.deleted', theme_name: theme_name)
73
+ else
74
+ I18n.t('subcommands.theme.delete.messages.cancelled')
75
+ end
76
+ Views::Shared::Info.new(messages: "\n#{message}").render
77
+ end
78
+ end
79
+
80
+ desc I18n.t('subcommands.theme.list.desc'), I18n.t('subcommands.theme.list.usage')
81
+ long_desc I18n.t('subcommands.theme.list.long_desc', themes_folder: Support::Fileable.themes_folder)
82
+ def list
83
+ Views::ColorTheme::Index.new.render
84
+ end
85
+
86
+ desc I18n.t('subcommands.theme.use.desc'), I18n.t('subcommands.theme.use.usage')
87
+ long_desc I18n.t('subcommands.theme.use.long_desc')
88
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
89
+ def use(theme_name = Models::ColorTheme::DEFAULT_THEME_NAME)
90
+ display_doto_header
91
+
92
+ return if Doto.env.local? && !Models::ColorTheme.exist?(theme_name: theme_name) && !create(theme_name)
93
+
94
+ unless Models::ColorTheme.exist?(theme_name: theme_name)
95
+ message = I18n.t('subcommands.theme.generic.errors.does_not_exist', theme_name: theme_name)
96
+ Views::Shared::Error.new(messages: message).render
97
+ return
98
+ end
99
+
100
+ change_theme theme_name: theme_name
101
+ # We need to display the header after the theme is updated so that it is displayed in the
102
+ # correct theme color.
103
+ message = I18n.t('subcommands.theme.use.messages.using_color_theme', theme_name: theme_name)
104
+ Views::Shared::Info.new(messages: message).render
105
+ end
106
+
107
+ desc I18n.t('subcommands.theme.show.desc'), I18n.t('subcommands.theme.show.usage')
108
+ long_desc I18n.t('subcommands.theme.show.long_desc')
109
+ def show(theme_name = configuration.theme_name)
110
+ if Doto::Models::ColorTheme.exist?(theme_name: theme_name)
111
+ Views::ColorTheme::Show.new(theme_name: theme_name).render
112
+ return
113
+ end
114
+
115
+ message = I18n.t('subcommands.theme.generic.errors.does_not_exist', theme_name: theme_name)
116
+ Views::Shared::Error.new(messages: message).render
117
+ end
118
+
119
+ private
120
+
121
+ def display_doto_header
122
+ self.class.display_doto_header
123
+ end
124
+
125
+ def change_theme(theme_name: Models::ColorTheme::DEFAULT_THEME_NAME)
126
+ configuration.theme_name = theme_name
127
+ configuration.save!
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+ require 'thor'
5
+
6
+ module Doto
7
+ module Support
8
+ module Ask
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
18
+ end
19
+
20
+ def yes?(prompt, options: {})
21
+ auto_prompt = auto_prompt(prompt, options)
22
+
23
+ unless auto_prompt.nil?
24
+ puts prompt
25
+ return auto_prompt
26
+ end
27
+
28
+ Thor::Base.shell.new.yes?(prompt)
29
+ end
30
+
31
+ private
32
+
33
+ def auto_prompt(prompt, options)
34
+ options = options.with_indifferent_access
35
+ prompt = Utils.strip_escapes(prompt)
36
+ @auto_prompt ||= begin
37
+ value = options.dig(:prompts, prompt) || options.dig(:prompts, :any)
38
+ value = (value == 'true' unless value.nil?)
39
+ value
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorized_string'
4
+
5
+ module Doto
6
+ module Support
7
+ module ColorThemable
8
+ def prompt_with_options(prompt:, options:)
9
+ # HACK: This module needs to be refactored to be more generic.
10
+ target_color_theme = defined?(color_theme) ? color_theme : self
11
+ options = "[#{options.join(',')}]"
12
+ "#{apply_theme(prompt, theme_color: target_color_theme.prompt)} " \
13
+ "#{apply_theme(options, theme_color: target_color_theme.prompt_options)}" \
14
+ "#{apply_theme('>', theme_color: target_color_theme.prompt)}"
15
+ end
16
+
17
+ module_function
18
+
19
+ def apply_theme(input, theme_color:)
20
+ if input.is_a?(Array)
21
+ return input.map do |string|
22
+ colorize_string(string, theme_color: theme_color)
23
+ end.join("\n")
24
+ end
25
+
26
+ colorize_string(input, theme_color: theme_color)
27
+ end
28
+
29
+ private
30
+
31
+ def colorize_string(input, theme_color:)
32
+ ColorizedString[input].colorize(**theme_color)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/color_theme'
4
+ require_relative '../support/color_themable'
5
+
6
+ module Doto
7
+ module Support
8
+ module CommandHelpColorizable
9
+ class << self
10
+ def included(base)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ module ClassMethods
15
+ # Handles general help colorization.
16
+ def help(shell, subcommand = false) # rubocop:disable Style/OptionalBooleanParameter
17
+ help_text = Services::StdoutRedirectorService.call { super }
18
+ puts apply_theme(help_text, theme_color: color_theme.help)
19
+ end
20
+
21
+ # Handles sub-command help colorization.
22
+ def command_help(shell, subcommand = false) # rubocop:disable Style/OptionalBooleanParameter
23
+ help_text = Services::StdoutRedirectorService.call { super }
24
+ puts apply_theme(help_text, theme_color: color_theme.help)
25
+ end
26
+
27
+ def color_theme
28
+ @color_theme ||= Models::ColorTheme.current_or_default
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end