ruby_todo 1.0.1 → 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.
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyTodo
4
- VERSION = "1.0.1"
4
+ VERSION = "1.0.4"
5
5
  end
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