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,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../env'
4
+ require_relative '../models/color_theme'
5
+ require_relative '../models/project'
6
+ require_relative '../services/stderr_redirector_service'
7
+ require_relative '../views/shared/error'
8
+ require_relative 'color_themable'
9
+
10
+ module Doto
11
+ module Support
12
+ module CommandHookable
13
+ class << self
14
+ def included(base)
15
+ base.extend(ColorThemable)
16
+ base.extend(ClassMethods)
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ def start(args = ARGV, options = {})
22
+ display_doto_header unless suspend_header?(args, options)
23
+ stderror = Services::StderrRedirectorService.call do
24
+ super
25
+ end
26
+ display_errors_if(stderror)
27
+ display_doto_footer
28
+ end
29
+
30
+ def display_doto_header
31
+ if Doto.env.screen_shot_mode?
32
+ puts apply_theme('Running screen shot mode!', theme_color: color_theme.warning)
33
+ puts "#{Doto.env.screen_shot_prompt} doto #{ARGV.join(' ')}"
34
+ end
35
+ end
36
+
37
+ def display_doto_footer
38
+ puts apply_theme('_' * 50, theme_color: color_theme.doto_footer)
39
+ # TODO: I18n.
40
+ puts apply_theme("doto v#{Doto::VERSION} | Project: #{project} | Theme: #{color_theme.theme_name}",
41
+ theme_color: color_theme.doto_footer)
42
+ end
43
+
44
+ private
45
+
46
+ def project
47
+ Models::Project.current_project_name
48
+ end
49
+
50
+ def suspend_header?(args, _options)
51
+ return false unless args.count > 1
52
+
53
+ # TODO: I18n?
54
+ true if args[0] == 'theme' && %w[use delete].include?(args[1])
55
+ end
56
+
57
+ def display_errors_if(stderror_string)
58
+ stderror_string = stderror_string.strip
59
+ return unless stderror_string.present?
60
+
61
+ errors = stderror_string.split("\n").map(&:strip)
62
+ Views::Shared::Error.new(messages: errors, options: options.merge({ ordered_list: false })).render
63
+ end
64
+
65
+ def color_theme
66
+ Models::ColorTheme.current_or_default
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'time'
4
+ require_relative 'time_mnemonic'
5
+
6
+ module Doto
7
+ module Support
8
+ module CommandOptions
9
+ module DotoTimes
10
+ module_function
11
+
12
+ # Returns an array of Time objects. The first element is the "from" time.
13
+ # The second element is the "to" time. Both arguments are expected to be
14
+ # command options that are time strings, time or relative time mnemonics.
15
+ def doto_times_for(from_option:, to_option:)
16
+ from_time = doto_from_time_for(from_option: from_option)
17
+ to_time = doto_to_time_for(to_option: to_option, from_time: from_time)
18
+
19
+ errors = []
20
+ errors << I18n.t('errors.from_option_invalid', from_option: from_option) if from_time.nil?
21
+ errors << I18n.t('errors.to_option_invalid', to_option: to_option) if to_time.nil?
22
+ return [[], errors] if errors.any?
23
+
24
+ min_time, max_time = [from_time, to_time].minmax
25
+ [(min_time.to_date..max_time.to_date).map(&:to_time), []]
26
+ end
27
+
28
+ def doto_from_time_for(from_option:)
29
+ return if from_option.nil?
30
+
31
+ from_time = if TimeMnemonic.time_mnemonic?(from_option)
32
+ TimeMnemonic.time_from_mnemonic(command_option: from_option)
33
+ end
34
+ from_time || Time.time_from_date_string(command_option: from_option)
35
+ end
36
+
37
+ def doto_to_time_for(to_option:, from_time:)
38
+ to_time = if TimeMnemonic.relative_time_mnemonic?(to_option)
39
+ TimeMnemonic.time_from_mnemonic(command_option: to_option, relative_time: from_time)
40
+ elsif TimeMnemonic.time_mnemonic?(to_option)
41
+ TimeMnemonic.time_from_mnemonic(command_option: to_option)
42
+ end
43
+ to_time || Time.time_from_date_string(command_option: to_option)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Support
5
+ module CommandOptions
6
+ # TODO: Make this into an ActiveModel class that uses validations.
7
+ #
8
+ # The purpose of this module is to take a command option that is a string and return a Time object.
9
+ # The command option is expected to be a date in the format of [M]M/[D]D[/YYYY]. MM and DD with
10
+ # leading zeroes is optional (i.e. only M and D are required), YYYY is optionl and will be replaced
11
+ # with the current year if not provided.
12
+ module Time
13
+ DATE_CAPTURE_REGEX = %r{\A(?<month>0?[1-9]|1[0-2])/(?<day>0?[1-9]|1\d|2\d|3[01])(?:/(?<year>\d{4}))?\z}
14
+
15
+ module_function
16
+
17
+ def time_from_date_string!(command_option:)
18
+ raise ArgumentError, 'command_option is nil.' if command_option.nil?
19
+ raise ArgumentError, 'command_option is blank.' if command_option.blank?
20
+
21
+ unless command_option.is_a?(String)
22
+ raise ArgumentError, "command_option is not a String: \"#{command_option}\"."
23
+ end
24
+
25
+ time_parts = time_parts_for(time_string: command_option)
26
+ return unless time_parts?(time_parts: time_parts)
27
+
28
+ valid_time!(time_parts: time_parts)
29
+
30
+ # This will rescue errors resulting from calling Date.strptime with an invalid date string,
31
+ # and return a more meaningful error message.
32
+ rescue DateTime::Error
33
+ raise ArgumentError, "command_option is not a valid date: \"#{command_option}\"."
34
+ end
35
+
36
+ def time_from_date_string(command_option:)
37
+ time_from_date_string!(command_option: command_option)
38
+ rescue ArgumentError
39
+ nil
40
+ end
41
+
42
+ # private_class_methods go here.
43
+
44
+ # This method returns the time parts for the given time string in a hash
45
+ # (i.e. month, day, year) IF the time string matches the DATE_CAPTURE_REGEX
46
+ # regex. Otherwise, it returns an empty hash.
47
+ def time_parts_for(time_string:)
48
+ match_data = DATE_CAPTURE_REGEX.match(time_string)
49
+ return {} if match_data.nil?
50
+
51
+ {
52
+ month: match_data[:month],
53
+ day: match_data[:day],
54
+ year: match_data[:year]
55
+ }
56
+ end
57
+
58
+ # This method returns true if the date passes the DATE_CAPTURE_REGEX regex match
59
+ # in #date_parts_for and returns a non-nil hash. Otherwise, it returns false.
60
+ # A non-nil hash returned from #date_parts_for doesn necessarily mean the date
61
+ # parts will equate to a valid date when parsed, it just means the date string
62
+ # matched the regex. Calling #valid_date! will raise an ArgumentError if the
63
+ # date parts do not equate to a valid date.
64
+ def time_parts?(time_parts:)
65
+ !time_parts.empty?
66
+ end
67
+
68
+ def valid_time!(time_parts:)
69
+ time_string = time_string_for(time_parts: time_parts)
70
+ # TODO: I18n.
71
+ Date.strptime(time_string, '%Y/%m/%d').to_time
72
+ end
73
+
74
+ def time_string_for(time_parts:)
75
+ # Replace the year with the current year if it is nil.
76
+ time_parts[:year] = ::Time.now.year if time_parts[:year].nil?
77
+ "#{time_parts[:year]}/#{time_parts[:month]}/#{time_parts[:day]}"
78
+ end
79
+
80
+ private_class_method :time_parts_for, :time_parts?, :valid_time!, :time_string_for
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'time_mnemonics'
4
+
5
+ module Doto
6
+ module Support
7
+ module CommandOptions
8
+ # The purpose of this module is to take a command option that is a string and return a Time object.
9
+ # The command option is expected to be a time mneumoic.
10
+ module TimeMnemonic
11
+ include TimeMnemonics
12
+
13
+ module_function
14
+
15
+ def time_from_mnemonic(command_option:, relative_time: nil)
16
+ time_from_mnemonic!(command_option: command_option, relative_time: relative_time)
17
+ rescue ArgumentError
18
+ nil
19
+ end
20
+
21
+ # command_option: is expected to me a time mnemonic. If relative_time is NOT nil, all
22
+ # time mnemonics are relative to relative_time. Otherwise, they are relative to Time.now.
23
+ # relative_time: is a Time object that is required IF command_option is expected to be
24
+ # a relative time mnemonic. Otherwise, it is optional.
25
+ def time_from_mnemonic!(command_option:, relative_time: nil)
26
+ validate_argument!(command_option: command_option, command_option_name: :command_option)
27
+ unless relative_time.nil? || relative_time.is_a?(::Time)
28
+ raise ArgumentError, "relative_time is not a Time object: \"#{relative_time}\""
29
+ end
30
+
31
+ relative_time ||= ::Time.now
32
+
33
+ time_for_mnemonic(mnemonic: command_option, relative_time: relative_time)
34
+ end
35
+
36
+ # This method returns true if mnemonic is a valid mnemonic OR
37
+ # a relative time mnemonic.
38
+ def time_mnemonic?(mnemonic)
39
+ mnemonic?(mnemonic) || relative_time_mnemonic?(mnemonic)
40
+ end
41
+
42
+ # This method returns true if mnemonic is a valid relative
43
+ # time mnemonic.
44
+ def relative_time_mnemonic?(mnemonic)
45
+ return false unless mnemonic.is_a?(String)
46
+
47
+ mnemonic.match?(RELATIVE_REGEX)
48
+ end
49
+
50
+ # Add private_class_methods here.
51
+
52
+ # Returns a Time object from a mnemonic.
53
+ def time_for_mnemonic(mnemonic:, relative_time:)
54
+ time = relative_time
55
+ if today_mnemonic?(mnemonic)
56
+ time
57
+ elsif tomorrow_mnemonic?(mnemonic)
58
+ time.tomorrow
59
+ elsif yesterday_mnemonic?(mnemonic)
60
+ time.yesterday
61
+ elsif relative_time_mnemonic?(mnemonic)
62
+ relative_time_for(days_from_now: mnemonic, time: time)
63
+ end
64
+ end
65
+
66
+ def relative_time_for(days_from_now:, time:)
67
+ days_from_now.to_i.days.from_now(time)
68
+ end
69
+
70
+ # This method returns true if mnemonic is a valid time mnemonic.
71
+ # This method will return false if mnemonic is an invalid mnemonic
72
+ # OR if mnemonic is a relative time mnemonic.
73
+ def mnemonic?(mnemonic)
74
+ today_mnemonic?(mnemonic) ||
75
+ tomorrow_mnemonic?(mnemonic) ||
76
+ yesterday_mnemonic?(mnemonic)
77
+ end
78
+
79
+ def today_mnemonic?(mnemonic)
80
+ TODAY.include?(mnemonic)
81
+ end
82
+
83
+ def tomorrow_mnemonic?(mnemonic)
84
+ TOMORROW.include?(mnemonic)
85
+ end
86
+
87
+ def yesterday_mnemonic?(mnemonic)
88
+ YESTERDAY.include?(mnemonic)
89
+ end
90
+
91
+ def validate_argument!(command_option:, command_option_name:)
92
+ raise ArgumentError, "#{command_option_name} cannot be nil." if command_option.nil?
93
+ raise ArgumentError, "#{command_option_name} cannot be blank." if command_option.blank?
94
+ unless command_option.is_a?(String)
95
+ raise ArgumentError, "#{command_option_name} must be a String: \"#{command_option}\""
96
+ end
97
+ unless time_mnemonic?(command_option)
98
+ raise ArgumentError, "#{command_option_name} is an invalid mnemonic: \"#{command_option}\"."
99
+ end
100
+ end
101
+
102
+ private_class_method :time_for_mnemonic, :relative_time_for,
103
+ :mnemonic?, :today_mnemonic?, :tomorrow_mnemonic?,
104
+ :yesterday_mnemonic?, :validate_argument!
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Support
5
+ module CommandOptions
6
+ module TimeMnemonics
7
+ # TODO: I18n.
8
+ TODAY = %w[n today].freeze
9
+ TOMORROW = %w[t tomorrow].freeze
10
+ YESTERDAY = %w[y yesterday].freeze
11
+
12
+ RELATIVE_REGEX = /\A[+-]\d+\z/
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'short_string'
4
+
5
+ module Doto
6
+ module Support
7
+ module Descriptable
8
+ class << self
9
+ def included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+ end
13
+
14
+ def short_description
15
+ return '' if description.blank?
16
+
17
+ self.class.short_description(string: description)
18
+ end
19
+
20
+ module ClassMethods
21
+ include ShortString
22
+
23
+ def short_description(string:, count: ShortString::SHORT_STRING_MAX_COUNT, elipsis: '...')
24
+ short_string(string: string, count: count, elipsis: elipsis)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/configuration'
4
+ require_relative '../presenters/entry_group/list/dates_presenter'
5
+ require_relative '../services/entry_group/browse_service'
6
+ require_relative '../services/entry_group/counter_service'
7
+ require_relative '../views/entry_group/list'
8
+
9
+ module Doto
10
+ module Support
11
+ module EntryGroupBrowsable
12
+ def browse_entry_groups(time:, options: {})
13
+ raise ArgumentError, 'time must be a Time object' unless time.is_a?(Time)
14
+ raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash)
15
+
16
+ options = configuration.to_h.merge(options).with_indifferent_access
17
+ times = browse_service(time: time, options: options).call
18
+ if times.empty? || (options.fetch(:include_all, false) && no_entries_for?(times: times, options: options))
19
+ display_no_entries_to_display_message time: time, options: options
20
+ return
21
+ end
22
+
23
+ output = Services::StdoutRedirectorService.call do
24
+ self.class.display_doto_header
25
+ header = browse_header_for(time: time, options: options)
26
+ Views::Shared::Info.new(messages: header).render
27
+ puts
28
+ presenter = Presenters::EntryGroup::List::DatesPresenter.new(times: times, options: options)
29
+ Views::EntryGroup::List.new(presenter: presenter).render
30
+ self.class.display_doto_footer
31
+ end
32
+ output_with_pager output: output, options: options
33
+ end
34
+
35
+ private
36
+
37
+ def no_entries_for?(times:, options:)
38
+ Services::EntryGroup::CounterService.new(times: times, options: options).call.zero?
39
+ end
40
+
41
+ def browse_header_for(time:, options:)
42
+ of, times = case options[:browse]
43
+ when :week
44
+ [
45
+ I18n.t('subcommands.browse.headers.week_of', week: time.beginning_of_week.to_date),
46
+ [time.beginning_of_week, time.end_of_week]
47
+ ]
48
+ when :month
49
+ [
50
+ I18n.t('subcommands.browse.headers.month_of', month: I18n.l(time, format: '%B')),
51
+ [time.beginning_of_month, time.end_of_month]
52
+ ]
53
+ when :year
54
+ [
55
+ I18n.t('subcommands.browse.headers.year_of', year: time.to_date.year),
56
+ [time.beginning_of_year, time.end_of_year]
57
+ ]
58
+ end
59
+
60
+ I18n.t('subcommands.browse.headers.browsing', of: of, from: times.min.to_date.to_s, to: times.max.to_date.to_s)
61
+ end
62
+
63
+ def output_with_pager(output:, options:)
64
+ if options[:pager] == false
65
+ puts output
66
+ return
67
+ end
68
+
69
+ pager_command = if RUBY_PLATFORM.match?(/win32|windows/i)
70
+ 'more' # Windows command
71
+ else
72
+ 'less' # Unix-like command
73
+ end
74
+
75
+ IO.popen(pager_command, 'w') do |pipe|
76
+ pipe.puts output
77
+ pipe.close_write
78
+ end
79
+ rescue Errno::ENOENT
80
+ message = "Operating system pager command (#{pager_command}) not found. Falling back to direct output."
81
+ Views::Shared::Error.new(messages: message).render
82
+ puts output
83
+ end
84
+
85
+ def display_no_entries_to_display_message(time:, options:)
86
+ case options[:browse]
87
+ when :week
88
+ Views::EntryGroup::Shared::NoEntriesToDisplayForWeekOf.new(time: time, options: options).render
89
+ when :month
90
+ Views::EntryGroup::Shared::NoEntriesToDisplayForMonthOf.new(time: time, options: options).render
91
+ when :year
92
+ Views::EntryGroup::Shared::NoEntriesToDisplayForYearOf.new(time: time, options: options).render
93
+ else
94
+ raise NotImplementedError, 'Unhandled option; ' \
95
+ "expected :week, :month, or :year but received #{options[:browse]}"
96
+ end
97
+ end
98
+
99
+ def browse_service(time:, options: {})
100
+ Services::EntryGroup::BrowseService.new(time: time, options: options)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Support
5
+ module FieldErrors
6
+ FIELD_FORMAT_ERROR = :field_format_error
7
+ FIELD_TYPE_ERROR = :field_type_error
8
+ FIELD_DUPLICATE_ERROR = :field_duplicate_error
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Support
5
+ module Fileable
6
+ MIGRATION_VERSION_FILE_NAME = 'migration_version.json'
7
+
8
+ def doto_folder
9
+ File.join(root_folder, 'doto')
10
+ end
11
+
12
+ # Configuration
13
+
14
+ def config_folder
15
+ root_folder
16
+ end
17
+
18
+ def config_file_name
19
+ '.doto'
20
+ end
21
+
22
+ def config_path
23
+ File.join(config_folder, config_file_name)
24
+ end
25
+
26
+ # Entries
27
+
28
+ def entries_folder
29
+ project_folder = project_folder_for(project_name: Models::Project.current_project_name)
30
+ File.join(project_folder, 'entries')
31
+ end
32
+
33
+ def entries_file_name(time:, file_name_format: nil)
34
+ file_name_format ||= '%Y-%m-%d.json'
35
+ time.strftime(file_name_format)
36
+ end
37
+
38
+ def entries_path(time:, file_name_format: nil)
39
+ File.join(entries_folder, entries_file_name(time: time, file_name_format: file_name_format))
40
+ end
41
+
42
+ # Themes
43
+
44
+ def themes_folder
45
+ File.join(doto_folder, 'themes')
46
+ end
47
+
48
+ def themes_path(theme_name:)
49
+ File.join(themes_folder, theme_file_name(theme_name: theme_name))
50
+ end
51
+
52
+ def theme_file_name(theme_name:)
53
+ "#{theme_name}.json"
54
+ end
55
+
56
+ # Migration
57
+
58
+ def migration_version_folder
59
+ File.join(doto_folder)
60
+ end
61
+
62
+ def migration_version_path
63
+ File.join(migration_version_folder, MIGRATION_VERSION_FILE_NAME)
64
+ end
65
+
66
+ # Base folders
67
+
68
+ def root_folder
69
+ Dir.home
70
+ end
71
+
72
+ def temp_folder
73
+ Dir.tmpdir
74
+ end
75
+
76
+ def gem_dir
77
+ Gem.loaded_specs['doto'].gem_dir
78
+ end
79
+
80
+ # Seed data files and folders
81
+
82
+ def seed_data_doto_folder_for(migration_version:)
83
+ File.join(gem_dir, 'lib/seed_data', migration_version.to_s, 'doto')
84
+ end
85
+
86
+ def seed_data_doto_configuration_for(migration_version:)
87
+ File.join(gem_dir, 'lib/seed_data', migration_version.to_s, '.doto')
88
+ end
89
+
90
+ # Projects
91
+
92
+ # Returns the folder where all the projects are stored.
93
+ def projects_folder
94
+ File.join(doto_folder, 'projects')
95
+ end
96
+
97
+ # Current project
98
+
99
+ # Contains the name of the file that contains the current
100
+ # doto project currently being used.
101
+ def current_project_file_name
102
+ 'current_project.json'
103
+ end
104
+
105
+ # The complete path to the current project file.
106
+ def current_project_file
107
+ File.join(doto_folder, current_project_file_name)
108
+ end
109
+
110
+ # Project helpers
111
+
112
+ # Returns the path of the project with the given name.
113
+ def project_folder_for(project_name:)
114
+ raise I18n.t('errors.project_name_invalid', project_name: '{{blank}}') if project_name.blank?
115
+
116
+ File.join(projects_folder, project_name)
117
+ end
118
+ alias project_folder project_folder_for
119
+
120
+ def project_file_for(project_name:)
121
+ project_folder = project_folder_for(project_name: project_name)
122
+
123
+ File.join(project_folder, 'project.json')
124
+ end
125
+ alias project_file project_file_for
126
+
127
+ # Backup folders
128
+
129
+ def backup_folder_for(migration_version:)
130
+ File.join(root_folder, "doto-#{migration_version}-backup")
131
+ end
132
+
133
+ extend self # rubocop:disable Style/ModuleFunction
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ module Support
5
+ module Presentable
6
+ def presenter
7
+ "Doto::Presenters::#{self.class.name.demodulize}Presenter".constantize.new(self, options: options)
8
+ end
9
+ end
10
+ end
11
+ end