doto 0.0.1.pre.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
require_relative '../../models/entry_group'
|
5
|
+
require_relative '../../models/project'
|
6
|
+
|
7
|
+
module Doto
|
8
|
+
module Services
|
9
|
+
module EntryGroup
|
10
|
+
# Expects a hash having the following format:
|
11
|
+
# {
|
12
|
+
# "Project 1 Name" => {
|
13
|
+
# "2023-12-29" => ["Entry 1 description", "Entry 2 description", ...],
|
14
|
+
# "2023-12-30" => ["Entry 1 description", ...],
|
15
|
+
# "2023-12-31" => ["Entry 1 description", ...]
|
16
|
+
# },
|
17
|
+
# "Project 2 Name" => {
|
18
|
+
# "2023-12-29" => ["Entry 1 description", "Entry 2 description", ...],
|
19
|
+
# "2023-12-30" => ["Entry 1 description", ...],
|
20
|
+
# "2023-12-31" => ["Entry 1 description", ...]
|
21
|
+
# }
|
22
|
+
# }
|
23
|
+
class ImporterService
|
24
|
+
include Support::Fileable
|
25
|
+
|
26
|
+
def initialize(import_projects:, options: {})
|
27
|
+
raise ArgumentError, 'Argument import_projects is blank' if import_projects.blank?
|
28
|
+
raise ArgumentError, 'Argument import_projects is not a Hash' unless import_projects.is_a?(Hash)
|
29
|
+
|
30
|
+
raise_if_more_than_one_project(import_projects)
|
31
|
+
|
32
|
+
@import_projects = import_projects
|
33
|
+
@options = options
|
34
|
+
end
|
35
|
+
|
36
|
+
def call
|
37
|
+
import!
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :import_projects, :options
|
43
|
+
|
44
|
+
def import!
|
45
|
+
project_entry_groups.each_pair do |entry_group_date, entry_descriptions|
|
46
|
+
entry_group_for(entry_group_date).tap do |entry_group|
|
47
|
+
entry_descriptions.each do |entry_description|
|
48
|
+
add_entry_group_entry_if(entry_group: entry_group, entry_description: entry_description)
|
49
|
+
end
|
50
|
+
|
51
|
+
import_messages[entry_group.time_yyyy_mm_dd] = []
|
52
|
+
|
53
|
+
unless entry_group.save
|
54
|
+
entry_group.errors.full_messages.each do |error|
|
55
|
+
import_messages[entry_group.time_yyyy_mm_dd] << error
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
import_messages
|
62
|
+
end
|
63
|
+
|
64
|
+
def entry_group_for(entry_group_date)
|
65
|
+
time = Time.parse(entry_group_date).in_time_zone
|
66
|
+
if merge?
|
67
|
+
Models::EntryGroup.find_or_initialize(time: time)
|
68
|
+
else
|
69
|
+
Models::EntryGroup.new(time: time, options: options)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_entry_group_entry_if(entry_group:, entry_description:)
|
74
|
+
entry = Models::Entry.new(description: entry_description)
|
75
|
+
return entry_group.entries << entry if replace?
|
76
|
+
return if entry_group.entries.include?(entry)
|
77
|
+
|
78
|
+
entry_group.entries << entry
|
79
|
+
end
|
80
|
+
|
81
|
+
def project_entry_groups
|
82
|
+
@project_entry_groups ||= if override_project?
|
83
|
+
import_projects.values.first || {}
|
84
|
+
else
|
85
|
+
import_projects.fetch(current_project_name, {})
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def merge?
|
90
|
+
options.fetch(:merge, true)
|
91
|
+
end
|
92
|
+
|
93
|
+
def replace?
|
94
|
+
!merge?
|
95
|
+
end
|
96
|
+
|
97
|
+
def override_project?
|
98
|
+
options.fetch(:override, false)
|
99
|
+
end
|
100
|
+
|
101
|
+
def import_messages
|
102
|
+
@import_messages ||= {}
|
103
|
+
end
|
104
|
+
|
105
|
+
def current_project_name
|
106
|
+
@current_project_name ||= Models::Project.current_project.project_name
|
107
|
+
end
|
108
|
+
|
109
|
+
def raise_if_more_than_one_project(import_projects)
|
110
|
+
return if import_projects.keys.one?
|
111
|
+
|
112
|
+
raise ArgumentError, 'Only one project can be imported at a time'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doto
|
4
|
+
module Services
|
5
|
+
module MigrationVersion
|
6
|
+
class HydratorService
|
7
|
+
def initialize(migration_version_hash:, options: {})
|
8
|
+
raise ArgumentError, 'migration_version_hash is nil' if migration_version_hash.nil?
|
9
|
+
|
10
|
+
unless migration_version_hash.is_a?(Hash)
|
11
|
+
raise ArgumentError,
|
12
|
+
"migration_version_hash is the wrong object type: \"#{migration_version_hash}\""
|
13
|
+
end
|
14
|
+
raise ArgumentError, 'options is nil' if options.nil?
|
15
|
+
raise ArgumentError, "options is the wrong object type:\"#{options}\"" unless options.is_a?(Hash)
|
16
|
+
|
17
|
+
@migration_version_hash = migration_version_hash.dup
|
18
|
+
@options = options || {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def call
|
22
|
+
hydrate
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :migration_version_hash, :options
|
28
|
+
|
29
|
+
def hydrate
|
30
|
+
migration_version_hash[:version] = migration_version_hash[:version].to_i
|
31
|
+
migration_version_hash
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../models/color_theme'
|
4
|
+
|
5
|
+
module Doto
|
6
|
+
module Services
|
7
|
+
module Project
|
8
|
+
class HydratorService
|
9
|
+
def initialize(project_hash:, options: {})
|
10
|
+
raise ArgumentError, 'project_hash is nil' if project_hash.nil?
|
11
|
+
|
12
|
+
unless project_hash.is_a?(Hash)
|
13
|
+
raise ArgumentError,
|
14
|
+
"project_hash is the wrong object type: \"#{project_hash}\""
|
15
|
+
end
|
16
|
+
raise ArgumentError, 'options is nil' if options.nil?
|
17
|
+
raise ArgumentError, "options is the wrong object type:\"#{options}\"" unless options.is_a?(Hash)
|
18
|
+
|
19
|
+
@project_hash = project_hash
|
20
|
+
@options = options || {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
Models::Project.new(**hydrate)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :project_hash, :options
|
30
|
+
|
31
|
+
# Not much going on here at all, but it's here for consistency.
|
32
|
+
# Perform any pre-processing of the project_hash here (e.g. symbolize keys,
|
33
|
+
# convert values to the correct type, etc.).
|
34
|
+
def hydrate
|
35
|
+
project_hash
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../models/project'
|
4
|
+
require_relative '../../support/fileable'
|
5
|
+
|
6
|
+
module Doto
|
7
|
+
module Services
|
8
|
+
module Project
|
9
|
+
class RenameService
|
10
|
+
def initialize(from_project_name:, to_project_name:, to_project_description:, options: {})
|
11
|
+
@from_project_name = from_project_name
|
12
|
+
@to_project_name = to_project_name
|
13
|
+
@to_project_description = to_project_description
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
validate!
|
19
|
+
|
20
|
+
# NOTE: The default and current states need to be captured before
|
21
|
+
# the project is renamed.
|
22
|
+
rename!(
|
23
|
+
make_default: Models::Project.default_project?(project_name: from_project_name),
|
24
|
+
make_current: Models::Project.current_project?(project_name: from_project_name)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :from_project_name, :to_project_name, :to_project_description, :options
|
31
|
+
|
32
|
+
def rename!(make_default:, make_current:)
|
33
|
+
move_project
|
34
|
+
|
35
|
+
Models::Project.update(project_name: to_project_name,
|
36
|
+
description: to_project_description, options: options).tap do |project|
|
37
|
+
project.default! if make_default
|
38
|
+
project.use! if make_current
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def move_project
|
43
|
+
FileUtils.mv(Support::Fileable.project_folder_for(project_name: from_project_name), temp_project_folder)
|
44
|
+
FileUtils.mv(temp_project_folder, Support::Fileable.project_folder_for(project_name: to_project_name))
|
45
|
+
end
|
46
|
+
|
47
|
+
def temp_project_folder
|
48
|
+
@temp_project_folder ||= Support::Fileable.project_folder_for(project_name: SecureRandom.uuid)
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate!
|
52
|
+
validate_from_project_name!
|
53
|
+
validate_to_project_name!
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_from_project_name!
|
57
|
+
unless Models::Project.project_file_exist?(project_name: from_project_name)
|
58
|
+
raise I18n.t('models.project.errors.does_not_exist', project_name: from_project_name)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate_to_project_name!
|
63
|
+
if Models::Project.project_file_exist?(project_name: to_project_name)
|
64
|
+
raise I18n.t('models.project.errors.new_project_already_exists', project_name: to_project_name)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doto
|
4
|
+
module Services
|
5
|
+
# This service captures $stderr, resirects it to a StringIO object,
|
6
|
+
# and returns the string value.
|
7
|
+
# https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
|
8
|
+
module StderrRedirectorService
|
9
|
+
class << self
|
10
|
+
def call
|
11
|
+
raise ArgumentError, 'no block was provided' unless block_given?
|
12
|
+
|
13
|
+
# The output stream must be an IO-like object. In this case we capture it in
|
14
|
+
# an in-memory IO object so we can return the string value. Any IO object can
|
15
|
+
# be used here.
|
16
|
+
string_io = StringIO.new
|
17
|
+
original_stderr, $stderr = $stderr, string_io # rubocop:disable Style/ParallelAssignment
|
18
|
+
yield
|
19
|
+
string_io.string
|
20
|
+
ensure
|
21
|
+
# Restore the original $stderr.
|
22
|
+
$stderr = original_stderr
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doto
|
4
|
+
module Services
|
5
|
+
# This service captures $stdout, resirects it to a StringIO object,
|
6
|
+
# and returns the string value.
|
7
|
+
# https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
|
8
|
+
module StdoutRedirectorService
|
9
|
+
class << self
|
10
|
+
def call
|
11
|
+
raise ArgumentError, 'no block was provided' unless block_given?
|
12
|
+
|
13
|
+
# The output stream must be an IO-like object. In this case we capture it in
|
14
|
+
# an in-memory IO object so we can return the string value. Any IO object can
|
15
|
+
# be used here.
|
16
|
+
string_io = StringIO.new
|
17
|
+
original_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
|
18
|
+
yield
|
19
|
+
string_io.string
|
20
|
+
ensure
|
21
|
+
# Restore the original $stdout.
|
22
|
+
$stdout = original_stdout
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doto
|
4
|
+
module Services
|
5
|
+
module TempFile
|
6
|
+
class ReaderService
|
7
|
+
def initialize(tmp_file_path:, options: {})
|
8
|
+
raise ArgumentError, 'tmp_file_path is nil' if tmp_file_path.nil?
|
9
|
+
raise ArgumentError, 'tmp_file_path is the wrong object type' unless tmp_file_path.is_a?(String)
|
10
|
+
raise ArgumentError, 'tmp_file_path is empty' if tmp_file_path.empty?
|
11
|
+
raise ArgumentError, 'tmp_file_path does not exist' unless File.exist?(tmp_file_path)
|
12
|
+
raise ArgumentError, 'options is nil' if options.nil?
|
13
|
+
raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
|
14
|
+
|
15
|
+
@tmp_file_path = tmp_file_path
|
16
|
+
@options = options || {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
raise ArgumentError, 'no block given' unless block_given?
|
21
|
+
|
22
|
+
File.foreach(tmp_file_path) do |line|
|
23
|
+
yield line.strip
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :tmp_file_path, :options
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
module Doto
|
6
|
+
module Services
|
7
|
+
module TempFile
|
8
|
+
class WriterService
|
9
|
+
def initialize(tmp_file_content:, options: {})
|
10
|
+
raise ArgumentError, 'tmp_file_content is nil' if tmp_file_content.nil?
|
11
|
+
raise ArgumentError, 'tmp_file_content is the wrong object type' unless tmp_file_content.is_a?(String)
|
12
|
+
raise ArgumentError, 'options is nil' if options.nil?
|
13
|
+
raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
|
14
|
+
|
15
|
+
@tmp_file_content = tmp_file_content
|
16
|
+
@options = options || {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
raise ArgumentError, 'no block given' unless block_given?
|
21
|
+
|
22
|
+
Tempfile.new('doto').tap do |file|
|
23
|
+
file.write("#{tmp_file_content}\n")
|
24
|
+
file.close
|
25
|
+
yield file.path
|
26
|
+
end.unlink
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :tmp_file_content, :options
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/console'
|
4
|
+
|
5
|
+
require_relative '../services/stdout_redirector_service'
|
6
|
+
require_relative '../support/command_options/doto_times'
|
7
|
+
require_relative '../support/command_options/time_mnemonic'
|
8
|
+
require_relative '../support/entry_group_browsable'
|
9
|
+
require_relative '../support/time_formatable'
|
10
|
+
require_relative '../views/entry_group/shared/no_entries_to_display'
|
11
|
+
require_relative '../views/shared/error'
|
12
|
+
require_relative 'base_subcommand'
|
13
|
+
|
14
|
+
module Doto
|
15
|
+
module Subcommands
|
16
|
+
class Browse < BaseSubcommand
|
17
|
+
include Support::EntryGroupBrowsable
|
18
|
+
include Support::CommandOptions::TimeMnemonic
|
19
|
+
include Support::TimeFormatable
|
20
|
+
|
21
|
+
# TODO: I18n.
|
22
|
+
map %w[w] => :week
|
23
|
+
map %w[m] => :month
|
24
|
+
map %w[y] => :year
|
25
|
+
|
26
|
+
class_option :pager, default: true, type: :boolean, hide: true, aliases: '-p'
|
27
|
+
class_option :include_all, default: nil, type: :boolean, aliases: '-a',
|
28
|
+
desc: I18n.t('options.include_all')
|
29
|
+
|
30
|
+
desc I18n.t('subcommands.browse.week.desc'), I18n.t('subcommands.browse.week.usage')
|
31
|
+
long_desc I18n.t('subcommands.browse.week.long_desc')
|
32
|
+
def week
|
33
|
+
browse_entry_groups time: Time.now, options: options.merge({ browse: :week })
|
34
|
+
end
|
35
|
+
|
36
|
+
desc I18n.t('subcommands.browse.month.desc'), I18n.t('subcommands.browse.month.usage')
|
37
|
+
long_desc I18n.t('subcommands.browse.month.long_desc')
|
38
|
+
def month
|
39
|
+
browse_entry_groups time: Time.now, options: options.merge({ browse: :month })
|
40
|
+
end
|
41
|
+
|
42
|
+
desc I18n.t('subcommands.browse.year.desc'), I18n.t('subcommands.browse.year.usage')
|
43
|
+
long_desc I18n.t('subcommands.browse.year.long_desc')
|
44
|
+
def year
|
45
|
+
browse_entry_groups time: Time.now, options: options.merge({ browse: :year })
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../models/configuration'
|
4
|
+
require_relative '../views/configuration/show'
|
5
|
+
require_relative '../views/shared/model_errors'
|
6
|
+
require_relative '../views/shared/success'
|
7
|
+
require_relative '../views/shared/warning'
|
8
|
+
require_relative 'base_subcommand'
|
9
|
+
|
10
|
+
module Doto
|
11
|
+
module Subcommands
|
12
|
+
class Config < BaseSubcommand
|
13
|
+
default_command :help
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def exit_on_failure?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
desc I18n.t('subcommands.config.info.desc'), I18n.t('subcommands.config.info.usage')
|
22
|
+
long_desc I18n.t('subcommands.config.info.long_desc')
|
23
|
+
def info
|
24
|
+
configuration = Models::Configuration.new
|
25
|
+
Views::Configuration::Show.new(config: configuration).call
|
26
|
+
end
|
27
|
+
|
28
|
+
if Doto.env.development?
|
29
|
+
desc I18n.t('subcommands.config.init.desc'), I18n.t('subcommands.config.init.usage')
|
30
|
+
long_desc I18n.t('subcommands.config.init.long_desc', home_folder: Doto::Support::Fileable.root_folder)
|
31
|
+
def init
|
32
|
+
exit 1 if configuration_errors_or_wanings?
|
33
|
+
|
34
|
+
Models::Configuration.default.tap do |configuration|
|
35
|
+
configuration.save!
|
36
|
+
messages = [I18n.t('messages.configuration_file.created',
|
37
|
+
configuration_file: Models::Configuration.config_file)]
|
38
|
+
Views::Shared::Success.new(messages: messages).render
|
39
|
+
Views::Configuration::Show.new(config: configuration).render
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
desc I18n.t('subcommands.config.delete.desc'), I18n.t('subcommands.config.delete.usage')
|
44
|
+
long_desc I18n.t('subcommands.config.delete.long_desc')
|
45
|
+
def delete
|
46
|
+
unless Models::Configuration.exist?
|
47
|
+
messages = [I18n.t('messages.configuration_file.does_not_exist',
|
48
|
+
configuration_file: Models::Configuration.config_file)]
|
49
|
+
Views::Shared::Warning.new(messages: messages).render
|
50
|
+
exit 1
|
51
|
+
end
|
52
|
+
Models::Configuration.delete!
|
53
|
+
messages = [I18n.t('messages.configuration_file.deleted',
|
54
|
+
configuration_file: Models::Configuration.config_file)]
|
55
|
+
Views::Shared::Success.new(messages: messages).render
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def configuration_errors_or_wanings?
|
62
|
+
if Models::Configuration.exist?
|
63
|
+
messages = [I18n.t('messages.configuration_file.already_exists',
|
64
|
+
configuration_file: Models::Configuration.config_file)]
|
65
|
+
Views::Shared::Warning.new(messages: messages).render
|
66
|
+
elsif !Dir.exist?(Models::Configuration.config_folder)
|
67
|
+
messages = [I18n.t('messages.configuration_file.destination_folder_does_not_exist',
|
68
|
+
configuration_file: Models::Configuration.config_file)]
|
69
|
+
Views::Shared::Error.new(messages: messages).render
|
70
|
+
else
|
71
|
+
configuration = Models::Configuration.default
|
72
|
+
return false if configuration.valid?
|
73
|
+
|
74
|
+
Views::Shared::ModelErrors.new(model: configuration).render
|
75
|
+
end
|
76
|
+
|
77
|
+
true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../services/entry_group/counter_service'
|
4
|
+
require_relative '../services/entry_group/deleter_service'
|
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/entry_group/shared/no_entries_to_display'
|
9
|
+
require_relative '../views/shared/error'
|
10
|
+
require_relative 'base_subcommand'
|
11
|
+
|
12
|
+
module Doto
|
13
|
+
module Subcommands
|
14
|
+
class Delete < BaseSubcommand
|
15
|
+
include Support::CommandOptions::TimeMnemonic
|
16
|
+
include Support::TimeFormatable
|
17
|
+
|
18
|
+
# TODO: I18n.
|
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.delete.today.desc'), I18n.t('subcommands.delete.today.usage')
|
26
|
+
long_desc I18n.t('subcommands.delete.today.long_desc')
|
27
|
+
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
28
|
+
def today
|
29
|
+
delete_entry_groups_if times: [Time.now], options: options
|
30
|
+
end
|
31
|
+
|
32
|
+
desc I18n.t('subcommands.delete.tomorrow.desc'), I18n.t('subcommands.delete.tomorrow.usage')
|
33
|
+
long_desc I18n.t('subcommands.delete.tomorrow.long_desc')
|
34
|
+
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
35
|
+
def tomorrow
|
36
|
+
delete_entry_groups_if times: [Time.now.tomorrow], options: options
|
37
|
+
end
|
38
|
+
|
39
|
+
desc I18n.t('subcommands.delete.yesterday.desc'), I18n.t('subcommands.delete.yesterday.usage')
|
40
|
+
long_desc I18n.t('subcommands.delete.yesterday.long_desc')
|
41
|
+
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
42
|
+
def yesterday
|
43
|
+
delete_entry_groups_if times: [Time.now.yesterday], options: options
|
44
|
+
end
|
45
|
+
|
46
|
+
desc I18n.t('subcommands.delete.date.desc'), I18n.t('subcommands.delete.date.usage')
|
47
|
+
long_desc I18n.t('subcommands.delete.date.long_desc',
|
48
|
+
date_option_description: date_option_description,
|
49
|
+
mnemonic_option_description: mnemonic_option_description)
|
50
|
+
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
51
|
+
def date(date_or_mnemonic)
|
52
|
+
time = if time_mnemonic?(date_or_mnemonic)
|
53
|
+
time_from_mnemonic(command_option: date_or_mnemonic)
|
54
|
+
else
|
55
|
+
Time.parse(date_or_mnemonic)
|
56
|
+
end
|
57
|
+
delete_entry_groups_if times: [time], options: options
|
58
|
+
rescue ArgumentError => e
|
59
|
+
Views::Shared::Error.new(messages: e.message).render
|
60
|
+
end
|
61
|
+
|
62
|
+
desc I18n.t('subcommands.delete.dates.desc'), I18n.t('subcommands.delete.dates.usage')
|
63
|
+
long_desc I18n.t('subcommands.delete.dates.long_desc',
|
64
|
+
date_option_description: date_option_description,
|
65
|
+
mnemonic_option_description: mnemonic_option_description)
|
66
|
+
option :from, type: :string, required: true, aliases: '-f', banner: 'DATE|MNEMONIC'
|
67
|
+
option :to, type: :string, required: true, aliases: '-t', banner: 'DATE|MNEMONIC'
|
68
|
+
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
69
|
+
def dates
|
70
|
+
options = configuration.to_h.merge(self.options).with_indifferent_access
|
71
|
+
times, errors = Support::CommandOptions::DotoTimes.doto_times_for(from_option: options[:from], to_option: options[:to]) # rubocop:disable Layout/LineLength
|
72
|
+
if errors.any?
|
73
|
+
Views::Shared::Error.new(messages: errors).render
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
times = times_sort(times: times, entries_display_order: options[:entries_display_order])
|
78
|
+
delete_entry_groups_if times: times, options: options
|
79
|
+
rescue ArgumentError => e
|
80
|
+
Views::Shared::Error.new(messages: e.message).render
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def delete_entry_groups_if(times:, options:)
|
86
|
+
prompt_string = I18n.t('subcommands.delete.prompts.are_you_sure',
|
87
|
+
dates: yyyy_mm_dd_or_through_for(times: times), count: total_entry_groups_for(times: times))
|
88
|
+
prompt = color_theme.prompt_with_options(prompt: prompt_string, options: %w[y N])
|
89
|
+
if yes?(prompt, options: options)
|
90
|
+
deleted_count = delete_entry_groups_for(times: times)
|
91
|
+
message = I18n.t('subcommands.delete.messages.deleted', count: deleted_count)
|
92
|
+
Views::Shared::Success.new(messages: message).render
|
93
|
+
else
|
94
|
+
message = I18n.t('subcommands.delete.messages.cancelled')
|
95
|
+
Views::Shared::Info.new(messages: message).render
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def delete_entry_groups_for(times:)
|
100
|
+
Services::EntryGroup::DeleterService.new(times: times).call
|
101
|
+
end
|
102
|
+
|
103
|
+
def total_entry_groups_for(times:)
|
104
|
+
Services::EntryGroup::CounterService.new(times: times).call
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|