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.
- checksums.yaml +7 -0
- data/.env.test +1 -0
- data/.reek.yml +20 -0
- data/.rspec +3 -0
- data/.rubocop.yml +206 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +179 -0
- data/LICENSE.txt +21 -0
- data/README.md +38 -0
- data/Rakefile +16 -0
- data/bin/console +36 -0
- data/bin/doto +3 -0
- data/bin/setup +18 -0
- data/exe/doto +33 -0
- data/lib/core/ruby/color_theme_colors.rb +16 -0
- data/lib/core/ruby/color_theme_mode.rb +42 -0
- data/lib/core/ruby/wrap_and_join.rb +31 -0
- data/lib/doto/base_cli.rb +56 -0
- data/lib/doto/cli.rb +131 -0
- data/lib/doto/command_services/add_entry_service.rb +50 -0
- data/lib/doto/crud/json_file.rb +161 -0
- data/lib/doto/env.rb +44 -0
- data/lib/doto/migration/base_service.rb +118 -0
- data/lib/doto/migration/migrator.rb +24 -0
- data/lib/doto/migration/raw_helpers/color_theme_hash.rb +13 -0
- data/lib/doto/migration/raw_helpers/configuration_hash.rb +15 -0
- data/lib/doto/migration/raw_helpers/entry_group_hash.rb +13 -0
- data/lib/doto/migration/raw_json_file.rb +15 -0
- data/lib/doto/migration/raw_json_files.rb +56 -0
- data/lib/doto/migration/v20230613121411/service.rb +94 -0
- data/lib/doto/migration/v20240210161248/service.rb +148 -0
- data/lib/doto/migration/version.rb +7 -0
- data/lib/doto/models/color_theme.rb +224 -0
- data/lib/doto/models/configuration.rb +185 -0
- data/lib/doto/models/entry.rb +63 -0
- data/lib/doto/models/entry_group.rb +223 -0
- data/lib/doto/models/migration_version.rb +49 -0
- data/lib/doto/models/project.rb +295 -0
- data/lib/doto/presenters/base_presenter.rb +32 -0
- data/lib/doto/presenters/base_presenter_ex.rb +15 -0
- data/lib/doto/presenters/color_theme_presenter.rb +52 -0
- data/lib/doto/presenters/color_theme_show_presenter.rb +55 -0
- data/lib/doto/presenters/configuration_presenter.rb +50 -0
- data/lib/doto/presenters/entry_group/list/date_presenter.rb +77 -0
- data/lib/doto/presenters/entry_group/list/dates_presenter.rb +60 -0
- data/lib/doto/presenters/entry_group/list/messages.rb +15 -0
- data/lib/doto/presenters/entry_group/list/nothing_to_list.rb +15 -0
- data/lib/doto/presenters/entry_group_presenter.rb +35 -0
- data/lib/doto/presenters/entry_presenter.rb +25 -0
- data/lib/doto/presenters/export/all_presenter.rb +44 -0
- data/lib/doto/presenters/export/dates_presenter.rb +55 -0
- data/lib/doto/presenters/import/all_presenter.rb +57 -0
- data/lib/doto/presenters/import/dates_presenter.rb +70 -0
- data/lib/doto/presenters/import/import_entry.rb +22 -0
- data/lib/doto/presenters/import/import_file.rb +33 -0
- data/lib/doto/presenters/project/create_presenter.rb +44 -0
- data/lib/doto/presenters/project/defaultable.rb +15 -0
- data/lib/doto/presenters/project/delete_by_number_presenter.rb +54 -0
- data/lib/doto/presenters/project/delete_presenter.rb +53 -0
- data/lib/doto/presenters/project/list_presenter.rb +24 -0
- data/lib/doto/presenters/project/rename_by_number_presenter.rb +63 -0
- data/lib/doto/presenters/project/rename_presenter.rb +57 -0
- data/lib/doto/presenters/project/use_by_number_presenter.rb +57 -0
- data/lib/doto/presenters/project/use_presenter.rb +56 -0
- data/lib/doto/services/color_theme/hydrator_service.rb +42 -0
- data/lib/doto/services/configuration/hydrator_service.rb +42 -0
- data/lib/doto/services/entry/hydrator_service.rb +33 -0
- data/lib/doto/services/entry_group/browse_service.rb +100 -0
- data/lib/doto/services/entry_group/counter_service.rb +32 -0
- data/lib/doto/services/entry_group/deleter_service.rb +35 -0
- data/lib/doto/services/entry_group/editor_service.rb +103 -0
- data/lib/doto/services/entry_group/exporter_service.rb +98 -0
- data/lib/doto/services/entry_group/hydrator_service.rb +37 -0
- data/lib/doto/services/entry_group/importer_service.rb +117 -0
- data/lib/doto/services/migration_version/hydrator_service.rb +36 -0
- data/lib/doto/services/project/hydrator_service.rb +40 -0
- data/lib/doto/services/project/rename_service.rb +70 -0
- data/lib/doto/services/stderr_redirector_service.rb +27 -0
- data/lib/doto/services/stdout_redirector_service.rb +27 -0
- data/lib/doto/services/temp_file/reader_service.rb +33 -0
- data/lib/doto/services/temp_file/writer_service.rb +35 -0
- data/lib/doto/subcommands/base_subcommand.rb +12 -0
- data/lib/doto/subcommands/browse.rb +49 -0
- data/lib/doto/subcommands/config.rb +81 -0
- data/lib/doto/subcommands/delete.rb +108 -0
- data/lib/doto/subcommands/edit.rb +48 -0
- data/lib/doto/subcommands/export.rb +62 -0
- data/lib/doto/subcommands/import.rb +72 -0
- data/lib/doto/subcommands/list.rb +95 -0
- data/lib/doto/subcommands/project.rb +146 -0
- data/lib/doto/subcommands/theme.rb +131 -0
- data/lib/doto/support/ask.rb +44 -0
- data/lib/doto/support/color_themable.rb +36 -0
- data/lib/doto/support/command_help_colorizeable.rb +34 -0
- data/lib/doto/support/command_hookable.rb +71 -0
- data/lib/doto/support/command_options/doto_times.rb +48 -0
- data/lib/doto/support/command_options/time.rb +84 -0
- data/lib/doto/support/command_options/time_mnemonic.rb +108 -0
- data/lib/doto/support/command_options/time_mnemonics.rb +16 -0
- data/lib/doto/support/descriptable.rb +29 -0
- data/lib/doto/support/entry_group_browsable.rb +104 -0
- data/lib/doto/support/field_errors.rb +11 -0
- data/lib/doto/support/fileable.rb +136 -0
- data/lib/doto/support/presentable.rb +11 -0
- data/lib/doto/support/project_file_system.rb +118 -0
- data/lib/doto/support/short_string.rb +24 -0
- data/lib/doto/support/time_comparable.rb +21 -0
- data/lib/doto/support/time_formatable.rb +65 -0
- data/lib/doto/support/times_sortable.rb +71 -0
- data/lib/doto/support/transform_project_name.rb +24 -0
- data/lib/doto/support/utils.rb +11 -0
- data/lib/doto/validators/color_theme_validator.rb +74 -0
- data/lib/doto/validators/description_validator.rb +51 -0
- data/lib/doto/validators/entries_validator.rb +77 -0
- data/lib/doto/validators/project_name_validator.rb +58 -0
- data/lib/doto/validators/time_validator.rb +25 -0
- data/lib/doto/validators/version_validator.rb +29 -0
- data/lib/doto/version.rb +6 -0
- data/lib/doto/views/base_list_view.rb +41 -0
- data/lib/doto/views/color_theme/index.rb +62 -0
- data/lib/doto/views/color_theme/show.rb +107 -0
- data/lib/doto/views/configuration/show.rb +41 -0
- data/lib/doto/views/entry_group/edit.rb +121 -0
- data/lib/doto/views/entry_group/list.rb +23 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display.rb +53 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display_for_month_of.rb +32 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display_for_week_of.rb +33 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display_for_year_of.rb +33 -0
- data/lib/doto/views/entry_group/show.rb +63 -0
- data/lib/doto/views/export.rb +82 -0
- data/lib/doto/views/import.rb +105 -0
- data/lib/doto/views/import_dates.rb +17 -0
- data/lib/doto/views/project/create.rb +87 -0
- data/lib/doto/views/project/delete.rb +96 -0
- data/lib/doto/views/project/delete_by_number.rb +19 -0
- data/lib/doto/views/project/list.rb +115 -0
- data/lib/doto/views/project/rename.rb +98 -0
- data/lib/doto/views/project/rename_by_number.rb +21 -0
- data/lib/doto/views/project/use.rb +97 -0
- data/lib/doto/views/project/use_by_number.rb +19 -0
- data/lib/doto/views/shared/error.rb +17 -0
- data/lib/doto/views/shared/info.rb +17 -0
- data/lib/doto/views/shared/message.rb +85 -0
- data/lib/doto/views/shared/model_errors.rb +32 -0
- data/lib/doto/views/shared/success.rb +17 -0
- data/lib/doto/views/shared/warning.rb +17 -0
- data/lib/doto.rb +33 -0
- data/lib/locales/en/active_record.yml +17 -0
- data/lib/locales/en/commands.yml +165 -0
- data/lib/locales/en/miscellaneous.yml +29 -0
- data/lib/locales/en/presenters.yml +19 -0
- data/lib/locales/en/services.yml +14 -0
- data/lib/locales/en/subcommands.yml +786 -0
- data/lib/seed_data/0/.todo +5 -0
- data/lib/seed_data/20230613121411/.doto +8 -0
- data/lib/seed_data/20230613121411/doto/migration_version.json +3 -0
- data/lib/seed_data/20230613121411/doto/themes/cherry.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/christmas.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/default.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/lemon.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/light.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/matrix.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/whiteout.json +79 -0
- data/lib/seed_data/20240210161248/.doto +9 -0
- data/lib/seed_data/20240210161248/doto/current_project.json +4 -0
- data/lib/seed_data/20240210161248/doto/migration_version.json +3 -0
- data/lib/seed_data/20240210161248/doto/projects/default/project.json +5 -0
- data/lib/seed_data/20240210161248/doto/themes/cherry.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/christmas.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/default.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/lemon.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/light.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/matrix.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/whiteout.json +79 -0
- data/sig/dsu.rbs +4 -0
- 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,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
|