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,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