ruby_todo 1.0.3 → 1.0.4
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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +60 -75
- data/ai_assistant_implementation.md +1 -1
- data/lib/ruby_todo/ai_assistant/command_processor.rb +127 -0
- data/lib/ruby_todo/ai_assistant/openai_integration.rb +178 -183
- data/lib/ruby_todo/ai_assistant/param_extractor.rb +64 -0
- data/lib/ruby_todo/ai_assistant/prompt_builder.rb +95 -0
- data/lib/ruby_todo/ai_assistant/task_creator.rb +161 -0
- data/lib/ruby_todo/cli.rb +128 -469
- data/lib/ruby_todo/commands/ai_assistant.rb +752 -287
- data/lib/ruby_todo/commands/ai_commands.rb +20 -0
- data/lib/ruby_todo/commands/notebook_commands.rb +39 -0
- data/lib/ruby_todo/commands/template_commands.rb +139 -0
- data/lib/ruby_todo/concerns/import_export.rb +138 -0
- data/lib/ruby_todo/concerns/statistics.rb +166 -0
- data/lib/ruby_todo/concerns/task_filters.rb +39 -0
- data/lib/ruby_todo/formatters/display_formatter.rb +80 -0
- data/lib/ruby_todo/models/task.rb +0 -7
- data/lib/ruby_todo/version.rb +1 -1
- data/lib/ruby_todo.rb +9 -0
- metadata +13 -8
- data/.env.template +0 -2
- data/lib/ruby_todo/ai_assistant/common_query_handler.rb +0 -378
- data/lib/ruby_todo/ai_assistant/task_management.rb +0 -331
- data/lib/ruby_todo/ai_assistant/task_search.rb +0 -365
- data/test_ai_assistant.rb +0 -55
- data/test_migration.rb +0 -55
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module AICommands
|
5
|
+
def ai_ask(*prompt_args)
|
6
|
+
prompt = prompt_args.join(" ")
|
7
|
+
ai_command.ask(prompt, verbose: options[:verbose], api_key: options[:api_key])
|
8
|
+
end
|
9
|
+
|
10
|
+
def ai_configure
|
11
|
+
ai_command.configure
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def ai_command
|
17
|
+
@ai_command ||= AIAssistantCommand.new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module NotebookCommands
|
5
|
+
def notebook_create(name)
|
6
|
+
Notebook.create(name: name)
|
7
|
+
puts "Created notebook: #{name}".green
|
8
|
+
end
|
9
|
+
|
10
|
+
def notebook_list
|
11
|
+
notebooks = Notebook.all
|
12
|
+
return say "No notebooks found".yellow if notebooks.empty?
|
13
|
+
|
14
|
+
table = TTY::Table.new(
|
15
|
+
header: ["ID", "Name", "Tasks", "Created At", "Default"],
|
16
|
+
rows: notebooks.map do |notebook|
|
17
|
+
[
|
18
|
+
notebook.id,
|
19
|
+
notebook.name,
|
20
|
+
notebook.tasks.count,
|
21
|
+
notebook.created_at,
|
22
|
+
notebook.is_default? ? "✓" : ""
|
23
|
+
]
|
24
|
+
end
|
25
|
+
)
|
26
|
+
puts table.render(:ascii)
|
27
|
+
end
|
28
|
+
|
29
|
+
def notebook_set_default(name)
|
30
|
+
notebook = Notebook.find_by(name: name)
|
31
|
+
if notebook
|
32
|
+
notebook.make_default!
|
33
|
+
say "Successfully set '#{name}' as the default notebook".green
|
34
|
+
else
|
35
|
+
say "Notebook '#{name}' not found".red
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module TemplateDisplay
|
5
|
+
def display_template_list(templates)
|
6
|
+
if templates.empty?
|
7
|
+
puts "No templates found. Create one with 'ruby_todo template create NAME'"
|
8
|
+
return
|
9
|
+
end
|
10
|
+
|
11
|
+
table = TTY::Table.new(
|
12
|
+
header: ["ID", "Name", "Title Pattern", "Notebook", "Priority", "Due Date Offset"],
|
13
|
+
rows: templates.map do |template|
|
14
|
+
[
|
15
|
+
template.id,
|
16
|
+
template.name,
|
17
|
+
template.title_pattern,
|
18
|
+
template.notebook&.name || "None",
|
19
|
+
template.priority || "None",
|
20
|
+
template.due_date_offset || "None"
|
21
|
+
]
|
22
|
+
end
|
23
|
+
)
|
24
|
+
|
25
|
+
puts table.render(:unicode, padding: [0, 1])
|
26
|
+
end
|
27
|
+
|
28
|
+
def display_template_details(template)
|
29
|
+
puts "Template Details:"
|
30
|
+
puts "ID: #{template.id}"
|
31
|
+
puts "Name: #{template.name}"
|
32
|
+
puts "Notebook: #{template.notebook&.name || "None"}"
|
33
|
+
puts "Title Pattern: #{template.title_pattern}"
|
34
|
+
puts "Description Pattern: #{template.description_pattern || "None"}"
|
35
|
+
puts "Tags Pattern: #{template.tags_pattern || "None"}"
|
36
|
+
puts "Priority: #{template.priority || "None"}"
|
37
|
+
puts "Due Date Offset: #{template.due_date_offset || "None"}"
|
38
|
+
puts "Created At: #{template.created_at}"
|
39
|
+
puts "Updated At: #{template.updated_at}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module TemplateCommands
|
44
|
+
include TemplateDisplay
|
45
|
+
|
46
|
+
def template_create(name)
|
47
|
+
notebook = nil
|
48
|
+
if options[:notebook]
|
49
|
+
notebook = RubyTodo::Notebook.find_by(name: options[:notebook])
|
50
|
+
unless notebook
|
51
|
+
puts "Notebook '#{options[:notebook]}' not found."
|
52
|
+
exit 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
template = RubyTodo::Template.new(
|
57
|
+
name: name,
|
58
|
+
notebook: notebook,
|
59
|
+
title_pattern: options[:title],
|
60
|
+
description_pattern: options[:description],
|
61
|
+
tags_pattern: options[:tags],
|
62
|
+
priority: options[:priority],
|
63
|
+
due_date_offset: options[:due]
|
64
|
+
)
|
65
|
+
|
66
|
+
if template.save
|
67
|
+
puts "Template '#{name}' created successfully."
|
68
|
+
else
|
69
|
+
puts "Error creating template: #{template.errors.full_messages.join(", ")}"
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def template_list
|
75
|
+
templates = RubyTodo::Template.all
|
76
|
+
display_template_list(templates)
|
77
|
+
end
|
78
|
+
|
79
|
+
def template_show(name)
|
80
|
+
template = RubyTodo::Template.find_by(name: name)
|
81
|
+
|
82
|
+
unless template
|
83
|
+
puts "Template '#{name}' not found."
|
84
|
+
exit 1
|
85
|
+
end
|
86
|
+
|
87
|
+
display_template_details(template)
|
88
|
+
end
|
89
|
+
|
90
|
+
def template_delete(name)
|
91
|
+
template = RubyTodo::Template.find_by(name: name)
|
92
|
+
|
93
|
+
unless template
|
94
|
+
puts "Template '#{name}' not found."
|
95
|
+
exit 1
|
96
|
+
end
|
97
|
+
|
98
|
+
if template.destroy
|
99
|
+
puts "Template '#{name}' deleted successfully."
|
100
|
+
else
|
101
|
+
puts "Error deleting template: #{template.errors.full_messages.join(", ")}"
|
102
|
+
exit 1
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def template_use(name, notebook_name)
|
107
|
+
template = RubyTodo::Template.find_by(name: name)
|
108
|
+
|
109
|
+
unless template
|
110
|
+
puts "Template '#{name}' not found."
|
111
|
+
exit 1
|
112
|
+
end
|
113
|
+
|
114
|
+
notebook = RubyTodo::Notebook.find_by(name: notebook_name)
|
115
|
+
|
116
|
+
unless notebook
|
117
|
+
puts "Notebook '#{notebook_name}' not found."
|
118
|
+
exit 1
|
119
|
+
end
|
120
|
+
|
121
|
+
replacements = {}
|
122
|
+
if options[:replacements]
|
123
|
+
options[:replacements].split(",").each do |r|
|
124
|
+
key, value = r.split(":")
|
125
|
+
replacements[key] = value if key && value
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
task = template.create_task(notebook, replacements)
|
130
|
+
|
131
|
+
if task.persisted?
|
132
|
+
puts "Task created successfully with ID: #{task.id}"
|
133
|
+
else
|
134
|
+
puts "Error creating task: #{task.errors.full_messages.join(", ")}"
|
135
|
+
exit 1
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module CsvExport
|
5
|
+
private
|
6
|
+
|
7
|
+
def export_multiple_notebooks_to_csv(data, csv)
|
8
|
+
csv << ["Notebook", "Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date",
|
9
|
+
"Created At", "Updated At"]
|
10
|
+
|
11
|
+
data["notebooks"].each do |notebook|
|
12
|
+
notebook_name = notebook["name"]
|
13
|
+
notebook["tasks"].each_with_index do |task, index|
|
14
|
+
csv << [
|
15
|
+
notebook_name,
|
16
|
+
index + 1,
|
17
|
+
task["title"],
|
18
|
+
task["description"],
|
19
|
+
task["status"],
|
20
|
+
task["priority"],
|
21
|
+
task["tags"],
|
22
|
+
task["due_date"],
|
23
|
+
task["created_at"],
|
24
|
+
task["updated_at"]
|
25
|
+
]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def export_single_notebook_to_csv(data, csv)
|
31
|
+
csv << ["Task ID", "Title", "Description", "Status", "Priority", "Tags", "Due Date", "Created At",
|
32
|
+
"Updated At"]
|
33
|
+
|
34
|
+
data["tasks"].each_with_index do |task, index|
|
35
|
+
csv << [
|
36
|
+
index + 1,
|
37
|
+
task["title"],
|
38
|
+
task["description"],
|
39
|
+
task["status"],
|
40
|
+
task["priority"],
|
41
|
+
task["tags"],
|
42
|
+
task["due_date"],
|
43
|
+
task["created_at"],
|
44
|
+
task["updated_at"]
|
45
|
+
]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
module ImportExport
|
51
|
+
include CsvExport
|
52
|
+
|
53
|
+
def export_notebook(notebook)
|
54
|
+
{
|
55
|
+
"name" => notebook.name,
|
56
|
+
"created_at" => notebook.created_at,
|
57
|
+
"updated_at" => notebook.updated_at,
|
58
|
+
"tasks" => notebook.tasks.map { |task| task_to_hash(task) }
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def export_all_notebooks(notebooks)
|
63
|
+
{
|
64
|
+
"notebooks" => notebooks.map { |notebook| export_notebook(notebook) }
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def task_to_hash(task)
|
69
|
+
{
|
70
|
+
"title" => task.title,
|
71
|
+
"description" => task.description,
|
72
|
+
"status" => task.status,
|
73
|
+
"priority" => task.priority,
|
74
|
+
"tags" => task.tags,
|
75
|
+
"due_date" => task.due_date&.iso8601,
|
76
|
+
"created_at" => task.created_at&.iso8601,
|
77
|
+
"updated_at" => task.updated_at&.iso8601
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def export_to_json(data, filename)
|
82
|
+
File.write(filename, JSON.pretty_generate(data))
|
83
|
+
end
|
84
|
+
|
85
|
+
def export_to_csv(data, filename)
|
86
|
+
CSV.open(filename, "wb") do |csv|
|
87
|
+
if data["notebooks"]
|
88
|
+
export_multiple_notebooks_to_csv(data, csv)
|
89
|
+
else
|
90
|
+
export_single_notebook_to_csv(data, csv)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def import_tasks(notebook, tasks_data)
|
96
|
+
count = 0
|
97
|
+
|
98
|
+
tasks_data.each do |task_data|
|
99
|
+
due_date = Time.parse(task_data["due_date"]) if task_data["due_date"]
|
100
|
+
|
101
|
+
task = Task.create(
|
102
|
+
notebook: notebook,
|
103
|
+
title: task_data["title"],
|
104
|
+
description: task_data["description"],
|
105
|
+
status: task_data["status"] || "todo",
|
106
|
+
priority: task_data["priority"],
|
107
|
+
tags: task_data["tags"],
|
108
|
+
due_date: due_date
|
109
|
+
)
|
110
|
+
|
111
|
+
count += 1 if task.persisted?
|
112
|
+
end
|
113
|
+
|
114
|
+
count
|
115
|
+
end
|
116
|
+
|
117
|
+
def import_all_notebooks(data)
|
118
|
+
results = { notebooks: 0, tasks: 0 }
|
119
|
+
|
120
|
+
data["notebooks"].each do |notebook_data|
|
121
|
+
notebook_name = notebook_data["name"]
|
122
|
+
notebook = Notebook.find_by(name: notebook_name)
|
123
|
+
|
124
|
+
unless notebook
|
125
|
+
notebook = Notebook.create(name: notebook_name)
|
126
|
+
results[:notebooks] += 1 if notebook.persisted?
|
127
|
+
end
|
128
|
+
|
129
|
+
if notebook.persisted?
|
130
|
+
tasks_count = import_tasks(notebook, notebook_data["tasks"])
|
131
|
+
results[:tasks] += tasks_count
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
results
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module StatisticsDisplay
|
5
|
+
private
|
6
|
+
|
7
|
+
def display_priority_distribution(tasks)
|
8
|
+
return if tasks.empty?
|
9
|
+
|
10
|
+
total = tasks.count
|
11
|
+
high_count = tasks.where(priority: "high").count
|
12
|
+
medium_count = tasks.where(priority: "medium").count
|
13
|
+
low_count = tasks.where(priority: "low").count
|
14
|
+
|
15
|
+
puts "\nPriority Distribution:"
|
16
|
+
puts "High: #{high_count} (#{percentage(high_count, total)}%)"
|
17
|
+
puts "Medium: #{medium_count} (#{percentage(medium_count, total)}%)"
|
18
|
+
puts "Low: #{low_count} (#{percentage(low_count, total)}%)"
|
19
|
+
end
|
20
|
+
|
21
|
+
def display_tag_distribution(tasks)
|
22
|
+
return if tasks.empty?
|
23
|
+
|
24
|
+
puts "\nTop Tags:"
|
25
|
+
tag_counts = Hash.new(0)
|
26
|
+
tasks.each do |task|
|
27
|
+
task.tags.split(",").each { |tag| tag_counts[tag.strip] += 1 } if task.tags
|
28
|
+
end
|
29
|
+
|
30
|
+
tag_counts.sort_by { |_, count| -count }.first(5).each do |tag, count|
|
31
|
+
puts "#{tag}: #{count} tasks (#{percentage(count, tasks.count)}%)"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def display_overdue_tasks(tasks)
|
36
|
+
return if tasks.empty?
|
37
|
+
|
38
|
+
overdue_count = tasks.select(&:overdue?).count
|
39
|
+
due_soon_count = tasks.select(&:due_soon?).count
|
40
|
+
total = tasks.count
|
41
|
+
|
42
|
+
puts "\nDue Date Status:"
|
43
|
+
puts "Overdue: #{overdue_count} (#{percentage(overdue_count, total)}%)"
|
44
|
+
puts "Due Soon: #{due_soon_count} (#{percentage(due_soon_count, total)}%)"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module Statistics
|
49
|
+
include StatisticsDisplay
|
50
|
+
|
51
|
+
def display_notebook_stats(notebook)
|
52
|
+
total_tasks = notebook.tasks.count
|
53
|
+
return puts "No tasks found in notebook '#{notebook.name}'" if total_tasks.zero?
|
54
|
+
|
55
|
+
todo_count = notebook.tasks.where(status: "todo").count
|
56
|
+
in_progress_count = notebook.tasks.where(status: "in_progress").count
|
57
|
+
done_count = notebook.tasks.where(status: "done").count
|
58
|
+
archived_count = notebook.tasks.where(status: "archived").count
|
59
|
+
|
60
|
+
puts "\nStatistics for notebook '#{notebook.name}':"
|
61
|
+
puts "Total tasks: #{total_tasks}"
|
62
|
+
puts "Todo: #{todo_count} (#{percentage(todo_count, total_tasks)}%)"
|
63
|
+
puts "In Progress: #{in_progress_count} (#{percentage(in_progress_count, total_tasks)}%)"
|
64
|
+
puts "Done: #{done_count} (#{percentage(done_count, total_tasks)}%)"
|
65
|
+
puts "Archived: #{archived_count} (#{percentage(archived_count, total_tasks)}%)"
|
66
|
+
|
67
|
+
display_priority_distribution(notebook.tasks)
|
68
|
+
display_tag_distribution(notebook.tasks)
|
69
|
+
display_overdue_tasks(notebook.tasks)
|
70
|
+
end
|
71
|
+
|
72
|
+
def display_global_stats
|
73
|
+
total_tasks = Task.count
|
74
|
+
return puts "No tasks found in any notebook" if total_tasks.zero?
|
75
|
+
|
76
|
+
display_global_stats_summary
|
77
|
+
end
|
78
|
+
|
79
|
+
def display_top_notebooks
|
80
|
+
puts "\nNotebook Statistics:"
|
81
|
+
notebook_stats = collect_notebook_stats
|
82
|
+
display_notebook_stats_table(notebook_stats)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def percentage(count, total)
|
88
|
+
return 0 if total.zero?
|
89
|
+
|
90
|
+
((count.to_f / total) * 100).round(1)
|
91
|
+
end
|
92
|
+
|
93
|
+
def display_global_stats_summary
|
94
|
+
total_tasks = Task.count
|
95
|
+
todo_count = Task.where(status: "todo").count
|
96
|
+
in_progress_count = Task.where(status: "in_progress").count
|
97
|
+
done_count = Task.where(status: "done").count
|
98
|
+
archived_count = Task.where(status: "archived").count
|
99
|
+
|
100
|
+
puts "\nGlobal Statistics:"
|
101
|
+
puts "Total tasks across all notebooks: #{total_tasks}"
|
102
|
+
puts "Todo: #{todo_count} (#{percentage(todo_count, total_tasks)}%)"
|
103
|
+
puts "In Progress: #{in_progress_count} (#{percentage(in_progress_count, total_tasks)}%)"
|
104
|
+
puts "Done: #{done_count} (#{percentage(done_count, total_tasks)}%)"
|
105
|
+
puts "Archived: #{archived_count} (#{percentage(archived_count, total_tasks)}%)"
|
106
|
+
|
107
|
+
display_priority_distribution(Task.all)
|
108
|
+
display_tag_distribution(Task.all)
|
109
|
+
display_overdue_tasks(Task.all)
|
110
|
+
display_top_notebooks
|
111
|
+
end
|
112
|
+
|
113
|
+
def collect_notebook_stats
|
114
|
+
notebook_data = Notebook.all.map do |notebook|
|
115
|
+
calculate_notebook_stats(notebook)
|
116
|
+
end
|
117
|
+
notebook_data.sort_by! { |data| -data[1] }
|
118
|
+
end
|
119
|
+
|
120
|
+
def calculate_notebook_stats(notebook)
|
121
|
+
task_count = notebook.tasks.count
|
122
|
+
todo_count = notebook.tasks.where(status: "todo").count
|
123
|
+
in_progress_count = notebook.tasks.where(status: "in_progress").count
|
124
|
+
done_count = notebook.tasks.where(status: "done").count
|
125
|
+
archived_count = notebook.tasks.where(status: "archived").count
|
126
|
+
|
127
|
+
[
|
128
|
+
notebook.name,
|
129
|
+
task_count,
|
130
|
+
todo_count,
|
131
|
+
in_progress_count,
|
132
|
+
done_count,
|
133
|
+
archived_count
|
134
|
+
]
|
135
|
+
end
|
136
|
+
|
137
|
+
def display_notebook_stats_table(notebook_stats)
|
138
|
+
headers = ["Notebook", "Total", "Todo", "In Progress", "Done", "Archived"]
|
139
|
+
rows = create_table_rows(notebook_stats)
|
140
|
+
render_stats_table(headers, rows)
|
141
|
+
end
|
142
|
+
|
143
|
+
def create_table_rows(notebook_stats)
|
144
|
+
notebook_stats.map do |stats|
|
145
|
+
name, total, todo, in_progress, done, archived = stats
|
146
|
+
[
|
147
|
+
name,
|
148
|
+
total.to_s,
|
149
|
+
"#{todo} (#{percentage(todo, total)}%)",
|
150
|
+
"#{in_progress} (#{percentage(in_progress, total)}%)",
|
151
|
+
"#{done} (#{percentage(done, total)}%)",
|
152
|
+
"#{archived} (#{percentage(archived, total)}%)"
|
153
|
+
]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def render_stats_table(headers, rows)
|
158
|
+
table = TTY::Table.new(header: headers, rows: rows)
|
159
|
+
puts table.render(:ascii, padding: [0, 1], width: TTY::Screen.width || 80)
|
160
|
+
rescue NoMethodError
|
161
|
+
# Fallback for non-TTY environments (like tests)
|
162
|
+
puts headers.join("\t")
|
163
|
+
rows.each { |row| puts row.join("\t") }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module TaskFilters
|
5
|
+
def apply_filters(tasks)
|
6
|
+
tasks = apply_status_filter(tasks)
|
7
|
+
tasks = apply_priority_filter(tasks)
|
8
|
+
tasks = apply_tag_filter(tasks)
|
9
|
+
apply_due_date_filters(tasks)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def apply_status_filter(tasks)
|
15
|
+
return tasks unless options[:status]
|
16
|
+
|
17
|
+
tasks.where(status: options[:status])
|
18
|
+
end
|
19
|
+
|
20
|
+
def apply_priority_filter(tasks)
|
21
|
+
return tasks unless options[:priority]
|
22
|
+
|
23
|
+
tasks.where(priority: options[:priority])
|
24
|
+
end
|
25
|
+
|
26
|
+
def apply_tag_filter(tasks)
|
27
|
+
return tasks unless options[:tags]
|
28
|
+
|
29
|
+
tag_filters = options[:tags].split(",").map(&:strip)
|
30
|
+
tasks.select { |t| t.tags && tag_filters.any? { |tag| t.tags.include?(tag) } }
|
31
|
+
end
|
32
|
+
|
33
|
+
def apply_due_date_filters(tasks)
|
34
|
+
tasks = tasks.select(&:due_soon?) if options[:due_soon]
|
35
|
+
tasks = tasks.select(&:overdue?) if options[:overdue]
|
36
|
+
tasks
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module DisplayFormatter
|
5
|
+
def format_status(status)
|
6
|
+
case status
|
7
|
+
when "todo" then "Todo".yellow
|
8
|
+
when "in_progress" then "In Progress".blue
|
9
|
+
when "done" then "Done".green
|
10
|
+
when "archived" then "Archived".gray
|
11
|
+
else status
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def format_priority(priority)
|
16
|
+
return nil unless priority
|
17
|
+
|
18
|
+
case priority.downcase
|
19
|
+
when "high" then "High".red
|
20
|
+
when "medium" then "Medium".yellow
|
21
|
+
when "low" then "Low".green
|
22
|
+
else priority
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def format_due_date(due_date)
|
27
|
+
return "No due date" unless due_date
|
28
|
+
|
29
|
+
if due_date < Time.now && due_date > Time.now - 24 * 60 * 60
|
30
|
+
"Today #{due_date.strftime("%H:%M")}".red
|
31
|
+
elsif due_date < Time.now
|
32
|
+
"Overdue #{due_date.strftime("%Y-%m-%d %H:%M")}".red
|
33
|
+
elsif due_date < Time.now + 24 * 60 * 60
|
34
|
+
"Today #{due_date.strftime("%H:%M")}".yellow
|
35
|
+
else
|
36
|
+
due_date.strftime("%Y-%m-%d %H:%M")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def truncate_text(text, length = 30)
|
41
|
+
return nil unless text
|
42
|
+
|
43
|
+
text.length > length ? "#{text[0...length]}..." : text
|
44
|
+
end
|
45
|
+
|
46
|
+
def display_tasks(tasks)
|
47
|
+
if ENV["RUBY_TODO_TEST"]
|
48
|
+
display_tasks_simple_format(tasks)
|
49
|
+
else
|
50
|
+
display_tasks_table_format(tasks)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def display_tasks_simple_format(tasks)
|
57
|
+
tasks.each do |t|
|
58
|
+
puts "#{t.id}: #{t.title} (#{t.status})"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def display_tasks_table_format(tasks)
|
63
|
+
table = TTY::Table.new(
|
64
|
+
header: ["ID", "Title", "Status", "Priority", "Due Date", "Tags", "Description"],
|
65
|
+
rows: tasks.map do |t|
|
66
|
+
[
|
67
|
+
t.id,
|
68
|
+
t.title,
|
69
|
+
format_status(t.status),
|
70
|
+
format_priority(t.priority),
|
71
|
+
format_due_date(t.due_date),
|
72
|
+
truncate_text(t.tags, 15),
|
73
|
+
truncate_text(t.description, 30)
|
74
|
+
]
|
75
|
+
end
|
76
|
+
)
|
77
|
+
puts table.render(:ascii)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -13,7 +13,6 @@ module RubyTodo
|
|
13
13
|
validate :due_date_cannot_be_in_past, if: :due_date?
|
14
14
|
validate :tags_format, if: :tags?
|
15
15
|
|
16
|
-
before_save :archive_completed_tasks
|
17
16
|
before_save :format_tags
|
18
17
|
|
19
18
|
scope :todo, -> { where(status: "todo") }
|
@@ -50,12 +49,6 @@ module RubyTodo
|
|
50
49
|
|
51
50
|
private
|
52
51
|
|
53
|
-
def archive_completed_tasks
|
54
|
-
return unless status_changed? && status == "done"
|
55
|
-
|
56
|
-
self.status = "archived"
|
57
|
-
end
|
58
|
-
|
59
52
|
def due_date_cannot_be_in_past
|
60
53
|
return unless due_date.present? && due_date < Time.current && new_record?
|
61
54
|
|
data/lib/ruby_todo/version.rb
CHANGED
data/lib/ruby_todo.rb
CHANGED
@@ -5,6 +5,15 @@ require_relative "ruby_todo/database"
|
|
5
5
|
require_relative "ruby_todo/models/notebook"
|
6
6
|
require_relative "ruby_todo/models/task"
|
7
7
|
require_relative "ruby_todo/models/template"
|
8
|
+
require_relative "ruby_todo/formatters/display_formatter"
|
9
|
+
require_relative "ruby_todo/concerns/statistics"
|
10
|
+
require_relative "ruby_todo/concerns/task_filters"
|
11
|
+
require_relative "ruby_todo/concerns/import_export"
|
12
|
+
require_relative "ruby_todo/commands/notebook_commands"
|
13
|
+
require_relative "ruby_todo/commands/template_commands"
|
14
|
+
require_relative "ruby_todo/commands/ai_commands"
|
15
|
+
require_relative "ruby_todo/commands/ai_assistant"
|
16
|
+
require_relative "ruby_todo/ai_assistant/openai_integration"
|
8
17
|
require_relative "ruby_todo/cli"
|
9
18
|
|
10
19
|
module RubyTodo
|