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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../env'
4
+ require_relative 'version'
5
+
6
+ module Doto
7
+ module Migration
8
+ class Migrator
9
+ class << self
10
+ def migrate_if!(migration_services: [])
11
+ return if migration_services.any? do |migration_service|
12
+ migration_service.migrate_if!
13
+ migration_service.class.migrates_to_latest_migration_version?
14
+ end
15
+
16
+ raise I18n.t('migrations.error.missing_current_migration_service', migration_version: Migration::VERSION)
17
+ rescue StandardError => e
18
+ puts I18n.t('migrations.error.failed', message: e.message)
19
+ exit 1 unless Doto.env.test?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Migration
5
+ module RawHelpers
6
+ module ColorThemeHash
7
+ def to_h
8
+ read.merge(version: version)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Migration
5
+ module RawHelpers
6
+ module ConfigurationHash
7
+ attr_accessor :default_project
8
+
9
+ def to_h
10
+ read.merge(version: version, default_project: default_project)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Migration
5
+ module RawHelpers
6
+ module EntryGroupHash
7
+ def to_h
8
+ read.merge(version: version)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../crud/json_file'
4
+
5
+ module Doto
6
+ module Migration
7
+ class RawJsonFile < Crud::JsonFile
8
+ public :read, :read!, :version=
9
+
10
+ def to_h
11
+ read.merge(version: version)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ require_relative 'raw_json_file'
7
+
8
+ module Doto
9
+ module Migration
10
+ class RawJsonFiles
11
+ attr_reader :folder
12
+
13
+ def initialize(folder)
14
+ @folder = folder
15
+ end
16
+
17
+ def each_file(regex: //)
18
+ return unless folder_exist?
19
+
20
+ Pathname.new(folder).children.each do |child|
21
+ next unless child.file? && child.to_s.match?(regex)
22
+
23
+ yield RawJsonFile.new(child)
24
+ end
25
+ end
26
+
27
+ def folder_exist?
28
+ self.class.folder_exist?(folder: folder)
29
+ end
30
+
31
+ class << self
32
+ def folder_exist?(folder:)
33
+ Dir.exist?(folder)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ attr_writer :folder
40
+
41
+ def safe_cp_r(source, destination)
42
+ Pathname.new(source).find do |source_path|
43
+ next if source_path.directory?
44
+
45
+ relative_path = source_path.relative_path_from(Pathname.new(source))
46
+ target_path = Pathname.new(destination).join(relative_path)
47
+
48
+ next if target_path.exist?
49
+
50
+ FileUtils.mkdir_p(target_path.dirname)
51
+ FileUtils.cp(source_path, target_path)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_service'
4
+ require_relative '../raw_helpers/entry_group_hash'
5
+ require_relative '../version'
6
+
7
+ module Doto
8
+ module Migration
9
+ module V20230613121411
10
+ class Service < BaseService
11
+ class << self
12
+ def from_migration_version
13
+ 0
14
+ end
15
+
16
+ def to_migration_version
17
+ 20230613121411 # rubocop:disable Style/NumericLiterals
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def run_migration!
24
+ super
25
+
26
+ raise_backup_folder_does_not_exist_error_if!
27
+
28
+ delete_old_config_file
29
+ delete_old_themes_folder
30
+
31
+ copy_new_doto_folders
32
+ copy_new_doto_configuration
33
+ migrate_entry_groups
34
+ end
35
+
36
+ def config_file_from
37
+ File.join(root_folder, '.doto')
38
+ end
39
+
40
+ def doto_folder_from
41
+ File.join(root_folder, 'doto')
42
+ end
43
+
44
+ def entries_folder_from
45
+ File.join(doto_folder_from, 'entries')
46
+ end
47
+
48
+ def themes_folder_from
49
+ File.join(doto_folder_from, 'themes')
50
+ end
51
+
52
+ def copy_new_doto_folders
53
+ puts 'Copying new doto folders...'
54
+
55
+ FileUtils.cp_r(File.join(seed_data_folder, '.'), File.join(root_folder, 'doto')) unless pretend?
56
+ end
57
+
58
+ def copy_new_doto_configuration
59
+ puts 'Copying new doto configuration...'
60
+
61
+ FileUtils.cp(seed_data_configuration, config_folder) unless pretend?
62
+ end
63
+
64
+ def migrate_entry_groups
65
+ puts 'Migrating entry groups...'
66
+
67
+ return if pretend?
68
+
69
+ puts "\tUpdating entry group version..."
70
+
71
+ RawJsonFiles.new(entries_folder_from).each_file(regex: /\d{4}-\d{2}-\d{2}.json/) do |raw_entry_group|
72
+ raw_entry_group.extend(RawHelpers::EntryGroupHash)
73
+ raw_entry_group.version = to_migration_version
74
+ raw_entry_group.save!
75
+ end
76
+ end
77
+
78
+ def delete_old_config_file
79
+ puts 'Deleting old configuration file...'
80
+
81
+ return if pretend?
82
+
83
+ File.delete(config_file_from) if File.file?(config_file_from)
84
+ end
85
+
86
+ def delete_old_themes_folder
87
+ puts 'Deleting old themes folder...'
88
+
89
+ FileUtils.rm_rf(themes_folder_from) unless pretend?
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_service'
4
+ require_relative '../raw_helpers/color_theme_hash'
5
+ require_relative '../raw_helpers/configuration_hash'
6
+ require_relative '../raw_helpers/entry_group_hash'
7
+ require_relative '../raw_json_file'
8
+ require_relative '../version'
9
+
10
+ module Doto
11
+ module Migration
12
+ module V20240210161248
13
+ class Service < BaseService
14
+ class << self
15
+ def from_migration_version
16
+ 20230613121411 # rubocop:disable Style/NumericLiterals
17
+ end
18
+
19
+ def to_migration_version
20
+ 20240210161248 # rubocop:disable Style/NumericLiterals
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def run_migration!
27
+ super
28
+
29
+ raise_backup_folder_does_not_exist_error_if!
30
+
31
+ add_new_color_themes
32
+ create_default_project
33
+ create_current_project_file
34
+ update_configuration
35
+ update_entry_groups
36
+ update_color_themes
37
+ delete_old_entry_folder
38
+
39
+ puts 'Migration completed successfully.'
40
+ end
41
+
42
+ def doto_folder_from
43
+ File.join(root_folder, 'doto')
44
+ end
45
+
46
+ def entries_folder_from
47
+ File.join(doto_folder_from, 'entries')
48
+ end
49
+
50
+ def themes_folder_from
51
+ File.join(doto_folder_from, 'themes')
52
+ end
53
+
54
+ def add_new_color_themes
55
+ puts 'Copying new color themes...'
56
+
57
+ FileUtils.mkdir_p(themes_folder_from) unless pretend?
58
+
59
+ %w[light.json christmas.json].each do |theme_file|
60
+ destination_theme_file_path = File.join(themes_folder_from, theme_file)
61
+ # Don't skip these theme files because they were deployed in the previous
62
+ # doto version with bugs. We need to overwrite them with this new version.
63
+ # next if File.exist?(destination_theme_file_path)
64
+
65
+ source_theme_file_path = File.join(seed_data_folder, 'themes', theme_file)
66
+ FileUtils.cp(source_theme_file_path, destination_theme_file_path) unless pretend?
67
+ end
68
+ end
69
+
70
+ def create_default_project
71
+ puts "Creating default project \"#{default_project_name}\"..."
72
+
73
+ return if pretend?
74
+
75
+ FileUtils.cp_r(File.join(seed_data_folder, 'projects', '.'),
76
+ File.join(doto_folder, 'projects'))
77
+ end
78
+
79
+ def create_current_project_file
80
+ puts 'Creating current project file...'
81
+
82
+ return if pretend?
83
+
84
+ # NOTE: doto_folder won't change and is safe to use here.
85
+ FileUtils.cp(File.join(seed_data_folder, 'current_project.json'), doto_folder)
86
+ end
87
+
88
+ def update_configuration
89
+ puts 'Updating configuration...'
90
+
91
+ return if pretend?
92
+
93
+ # NOTE: config_path won't change and is safe to use here.
94
+ RawJsonFile.new(config_path).tap do |configuration_file|
95
+ configuration_file.extend(RawHelpers::ConfigurationHash)
96
+ configuration_file.version = to_migration_version
97
+ configuration_file.default_project = default_project_name
98
+ end.save!
99
+ end
100
+
101
+ def update_entry_groups
102
+ puts 'Updating entry groups...'
103
+
104
+ return if pretend?
105
+
106
+ puts "\tCopying entries to default project \"#{default_project_name}\"..."
107
+
108
+ entries_folder_to = File.join(doto_folder, 'projects', default_project_name, 'entries')
109
+ FileUtils.cp_r(File.join(entries_folder_from, '.'), entries_folder_to)
110
+
111
+ puts "\tUpdating entry group version..."
112
+
113
+ RawJsonFiles.new(entries_folder_to).each_file(regex: /\d{4}-\d{2}-\d{2}.json/) do |raw_entry_group|
114
+ raw_entry_group.extend(RawHelpers::EntryGroupHash)
115
+ raw_entry_group.version = to_migration_version
116
+ raw_entry_group.save!
117
+ end
118
+ end
119
+
120
+ def update_color_themes
121
+ puts 'Updating color themes...'
122
+
123
+ FileUtils.cp_r(File.join(backup_folder, 'themes', '.'), themes_folder) unless pretend?
124
+
125
+ puts "\tUpdating color theme version..."
126
+
127
+ themes_folder_to = File.join(doto_folder, 'themes')
128
+
129
+ RawJsonFiles.new(themes_folder_to).each_file(regex: /.+.json/) do |raw_entry_group|
130
+ raw_entry_group.extend(RawHelpers::ColorThemeHash)
131
+ raw_entry_group.version = to_migration_version
132
+ raw_entry_group.save! unless pretend?
133
+ end
134
+ end
135
+
136
+ def delete_old_entry_folder
137
+ puts 'Cleaning up old entries...'
138
+
139
+ FileUtils.rm_rf(File.join(entries_folder_from)) unless pretend?
140
+ end
141
+
142
+ def default_project_name
143
+ 'default'
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Migration
5
+ VERSION = 20240210161248 # rubocop:disable Style/NumericLiterals
6
+ end
7
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../crud/json_file'
4
+ require_relative '../migration/version'
5
+ require_relative '../support/color_themable'
6
+ require_relative '../support/descriptable'
7
+ require_relative '../support/fileable'
8
+ require_relative '../support/presentable'
9
+ require_relative '../validators/color_theme_validator'
10
+ require_relative '../validators/description_validator'
11
+ require_relative '../validators/version_validator'
12
+ require_relative 'configuration'
13
+
14
+ module Doto
15
+ module Models
16
+ # This class represents a doto color theme.
17
+ class ColorTheme < Crud::JsonFile
18
+ include Support::ColorThemable
19
+ include Support::Descriptable
20
+ include Support::Fileable
21
+ include Support::Presentable
22
+
23
+ THEME_FILE_NAME_REGEX = /.+.json/
24
+
25
+ VERSION = Migration::VERSION
26
+
27
+ DEFAULT_THEME_NAME = 'default'
28
+ # Theme colors key/value pair format:
29
+ # <key>: { color: <color> [, mode: <mode>] [, background: <background>] }
30
+ # Where <color> (required) == any color represented in the colorize gem `String.colors` array.
31
+ # <mode> (optional, default is :default) == any mode represented in the colorize gem `String.modes` array.
32
+ # <background> (optional, default is :default) == any color represented in the colorize gem
33
+ # `String.colors` array.
34
+ DEFAULT_THEME_COLORS = {
35
+ help: { color: :cyan },
36
+ doto_header: { color: :white, mode: :bold, background: :cyan },
37
+ doto_footer: { color: :cyan },
38
+ header: { color: :cyan, mode: :bold },
39
+ subheader: { color: :cyan, mode: :underline },
40
+ body: { color: :cyan },
41
+ footer: { color: :light_cyan },
42
+ date: { color: :cyan, mode: :bold },
43
+ index: { color: :light_cyan },
44
+ # Status colors.
45
+ info: { color: :cyan },
46
+ success: { color: :green },
47
+ warning: { color: :yellow },
48
+ error: { color: :light_yellow, background: :red },
49
+ # Prompts
50
+ prompt: { color: :cyan, mode: :bold },
51
+ prompt_options: { color: :white, mode: :bold }
52
+ }.freeze
53
+ DEFAULT_THEME = {
54
+ version: VERSION,
55
+ description: 'Default theme.'
56
+ }.merge(DEFAULT_THEME_COLORS).freeze
57
+
58
+ MIN_DESCRIPTION_LENGTH = 2
59
+ MAX_DESCRIPTION_LENGTH = 256
60
+
61
+ # TODO: Validate other attrs.
62
+ validates_with Validators::DescriptionValidator
63
+ validates_with Validators::ColorThemeValidator
64
+ validates_with Validators::VersionValidator
65
+
66
+ attr_reader :theme_name, :options
67
+
68
+ def initialize(theme_name:, theme_hash: nil, options: {})
69
+ raise ArgumentError, 'theme_name is nil.' if theme_name.nil?
70
+ raise ArgumentError, "theme_name is the wrong object type: \"#{theme_name}\"." unless theme_name.is_a?(String)
71
+ unless theme_hash.is_a?(Hash) || theme_hash.nil?
72
+ raise ArgumentError, "theme_hash is the wrong object type: \"#{theme_hash}\"."
73
+ end
74
+
75
+ FileUtils.mkdir_p themes_folder
76
+
77
+ @theme_name = theme_name
78
+ @options = options || {}
79
+
80
+ super(self.class.send(:themes_path, theme_name: @theme_name))
81
+
82
+ theme_hash ||= DEFAULT_THEME.merge(description: "#{@theme_name.capitalize} theme")
83
+
84
+ # Color themes I expect will change a lot, so we're using
85
+ # a little meta-programming here to dynamically create
86
+ # public attr_readers and private attr_writers based on the
87
+ # keys in DEFAULT_THEME, then assign those attributes from
88
+ # the values in theme_hash. theme_hash will be guaranteed to
89
+ # have the same keys as DEFAULT_THEME.keys at this point
90
+ # because we called ensure_theme_hash! above.
91
+ DEFAULT_THEME.each_key do |attr|
92
+ self.class.class_eval do
93
+ attr_reader attr
94
+ attr_writer attr
95
+ private :"#{attr}="
96
+ end
97
+ attr_value = theme_hash[attr]
98
+ attr_value = attr_value.merge_default_colors if default_theme_color_keys.include?(attr)
99
+ send(:"#{attr}=", attr_value)
100
+ end
101
+ end
102
+
103
+ def delete
104
+ self.class.delete(theme_name: theme_name)
105
+ end
106
+
107
+ def delete!
108
+ self.class.delete!(theme_name: theme_name)
109
+ end
110
+
111
+ def exist?
112
+ self.class.exist?(theme_name: theme_name)
113
+ end
114
+
115
+ class << self
116
+ delegate :themes_folder, :themes_path, to: Support::Fileable
117
+
118
+ def all
119
+ Dir.glob("#{themes_folder}/*").map do |file_path|
120
+ theme_name = File.basename(file_path, '.*')
121
+ find(theme_name: theme_name)
122
+ end
123
+ end
124
+
125
+ def configuration
126
+ Models::Configuration.new
127
+ end
128
+
129
+ def current
130
+ theme_name = configuration.theme_name
131
+ return unless exist?(theme_name: theme_name)
132
+
133
+ find(theme_name: theme_name)
134
+ end
135
+
136
+ # Returns the current color theme if it exists; otherwise,
137
+ # it returns the default color theme.
138
+ def current_or_default
139
+ current || default
140
+ end
141
+
142
+ def default
143
+ new(theme_name: DEFAULT_THEME_NAME, theme_hash: DEFAULT_THEME)
144
+ end
145
+
146
+ def delete(theme_name:)
147
+ superclass.delete(file_path: themes_path(theme_name: theme_name))
148
+ end
149
+
150
+ def delete!(theme_name:)
151
+ superclass.delete!(file_path: themes_path(theme_name: theme_name))
152
+ end
153
+
154
+ def ensure_color_theme_color_defaults_for(theme_hash: DEFAULT_THEME)
155
+ theme_hash = theme_hash.dup
156
+
157
+ theme_hash.each_pair do |key, value|
158
+ next unless default_theme_color_keys.include?(key)
159
+
160
+ theme_hash[key] = value.merge_default_colors
161
+ end
162
+ theme_hash
163
+ end
164
+
165
+ def exist?(theme_name:)
166
+ superclass.file_exist?(file_path: themes_path(theme_name: theme_name))
167
+ end
168
+
169
+ def find(theme_name:)
170
+ theme_hash = read!(file_path: themes_path(theme_name: theme_name))
171
+ Services::ColorTheme::HydratorService.new(theme_name: theme_name, theme_hash: theme_hash).call
172
+ end
173
+
174
+ def find_or_create(theme_name:)
175
+ return find(theme_name: theme_name) if exist?(theme_name: theme_name)
176
+
177
+ new(theme_name: theme_name).tap(&:write!)
178
+ end
179
+
180
+ def find_or_initialize(theme_name:)
181
+ return find(theme_name: theme_name) if exist?(theme_name: theme_name)
182
+
183
+ new(theme_name: theme_name)
184
+ end
185
+
186
+ private
187
+
188
+ def default_theme_color_keys
189
+ DEFAULT_THEME_COLORS.keys
190
+ end
191
+ end
192
+
193
+ def to_h
194
+ {}.tap do |hash|
195
+ DEFAULT_THEME.each_key do |key|
196
+ hash[key] = public_send(key)
197
+ end
198
+ end
199
+ end
200
+
201
+ def ==(other)
202
+ return false unless other.is_a?(self.class)
203
+ return false unless other.theme_name == theme_name
204
+
205
+ DEFAULT_THEME.keys.all? { |key| public_send(key) == other.public_send(key) }
206
+ end
207
+ alias eql? ==
208
+
209
+ def hash
210
+ DEFAULT_THEME.keys.map { |key| public_send(key) }.tap do |hashes|
211
+ hashes << theme_name.hash
212
+ end.hash
213
+ end
214
+
215
+ private
216
+
217
+ attr_writer :theme_name, :description
218
+
219
+ def default_theme_color_keys
220
+ @default_theme_color_keys ||= self.class.send(:default_theme_color_keys)
221
+ end
222
+ end
223
+ end
224
+ end