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