ruby_todo 0.4.1 → 1.0.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 +4 -4
- data/.env.template +2 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +10 -0
- data/README.md +56 -72
- data/ai_assistant_implementation.md +611 -0
- data/db/migrate/20240328_add_is_default_to_notebooks.rb +10 -0
- data/delete_notebooks.rb +20 -0
- data/implementation_steps.md +130 -0
- data/lib/ruby_todo/ai_assistant/common_query_handler.rb +378 -0
- data/lib/ruby_todo/ai_assistant/configuration_management.rb +27 -0
- data/lib/ruby_todo/ai_assistant/openai_integration.rb +333 -0
- data/lib/ruby_todo/ai_assistant/task_creation.rb +86 -0
- data/lib/ruby_todo/ai_assistant/task_management.rb +327 -0
- data/lib/ruby_todo/ai_assistant/task_search.rb +362 -0
- data/lib/ruby_todo/cli.rb +296 -146
- data/lib/ruby_todo/commands/ai_assistant.rb +449 -0
- data/lib/ruby_todo/database.rb +58 -84
- data/lib/ruby_todo/models/notebook.rb +44 -10
- data/lib/ruby_todo/version.rb +1 -1
- data/progress_ai_test.md +126 -0
- data/protectors_tasks.json +159 -0
- data/test_ai_assistant.rb +55 -0
- data/test_migration.rb +55 -0
- metadata +46 -1
@@ -0,0 +1,130 @@
|
|
1
|
+
# Implementation Guide: Adding AI Assistant to Ruby Todo
|
2
|
+
|
3
|
+
This guide outlines the specific steps to add AI assistant functionality to the Ruby Todo gem, allowing users to interact with the application using natural language through Claude or OpenAI.
|
4
|
+
|
5
|
+
## Step 1: Update the gemspec file
|
6
|
+
|
7
|
+
Edit `ruby_todo.gemspec` to add the required dependencies:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# Runtime dependencies
|
11
|
+
spec.add_dependency "anthropic", "~> 0.1.0" # For Claude API
|
12
|
+
spec.add_dependency "ruby-openai", "~> 6.3.0" # For OpenAI API
|
13
|
+
spec.add_dependency "dotenv", "~> 2.8" # For API key management
|
14
|
+
```
|
15
|
+
|
16
|
+
## Step 2: Create the folder structure
|
17
|
+
|
18
|
+
```bash
|
19
|
+
mkdir -p lib/ruby_todo/commands
|
20
|
+
```
|
21
|
+
|
22
|
+
## Step 3: Create the AI Assistant command file
|
23
|
+
|
24
|
+
Create a new file at `lib/ruby_todo/commands/ai_assistant.rb` with the implementation from the plan document.
|
25
|
+
|
26
|
+
## Step 4: Update the CLI class
|
27
|
+
|
28
|
+
Edit `lib/ruby_todo/cli.rb` to add the AI Assistant command:
|
29
|
+
|
30
|
+
1. Add the require statement at the top:
|
31
|
+
```ruby
|
32
|
+
require_relative "commands/ai_assistant"
|
33
|
+
```
|
34
|
+
|
35
|
+
2. Register the subcommand inside the CLI class:
|
36
|
+
```ruby
|
37
|
+
# Register AI Assistant subcommand
|
38
|
+
desc "ai SUBCOMMAND", "Use AI assistant"
|
39
|
+
subcommand "ai", AIAssistantCommand
|
40
|
+
```
|
41
|
+
|
42
|
+
## Step 5: Create .env template
|
43
|
+
|
44
|
+
Create a `.env.template` file in the project root:
|
45
|
+
|
46
|
+
```
|
47
|
+
# Claude API key (if using Claude)
|
48
|
+
ANTHROPIC_API_KEY=your_claude_api_key_here
|
49
|
+
|
50
|
+
# OpenAI API key (if using OpenAI)
|
51
|
+
OPENAI_API_KEY=your_openai_api_key_here
|
52
|
+
```
|
53
|
+
|
54
|
+
Add `.env` to `.gitignore` to prevent accidental check-in of API keys.
|
55
|
+
|
56
|
+
## Step 6: Update README
|
57
|
+
|
58
|
+
Add the AI Assistant documentation to the README.md file:
|
59
|
+
|
60
|
+
1. Add "AI Assistant" to the Features list at the top
|
61
|
+
2. Add the full AI Assistant section after the Templates section
|
62
|
+
|
63
|
+
## Step 7: Create tests
|
64
|
+
|
65
|
+
Create tests for the AI Assistant functionality:
|
66
|
+
|
67
|
+
1. Create a new file `test/ai_assistant_test.rb`
|
68
|
+
2. Add unit tests for the AI Assistant command class
|
69
|
+
3. Add integration tests for the CLI integration
|
70
|
+
4. Add mock tests for API interactions
|
71
|
+
|
72
|
+
## Step 8: Install and test
|
73
|
+
|
74
|
+
1. Install the updated gem: `bundle exec rake install`
|
75
|
+
2. Run `ruby_todo ai configure` to set up your API key
|
76
|
+
3. Test with a simple prompt: `ruby_todo ai ask "Create a task in the Work notebook"`
|
77
|
+
|
78
|
+
## Usage Examples
|
79
|
+
|
80
|
+
Here are some examples of how to use the AI assistant:
|
81
|
+
|
82
|
+
1. Configure the AI assistant:
|
83
|
+
```bash
|
84
|
+
ruby_todo ai configure
|
85
|
+
```
|
86
|
+
|
87
|
+
2. Create a task using natural language:
|
88
|
+
```bash
|
89
|
+
ruby_todo ai ask "Add a high priority task to update documentation in my Work notebook due next Friday"
|
90
|
+
```
|
91
|
+
|
92
|
+
3. Move tasks to a different status:
|
93
|
+
```bash
|
94
|
+
ruby_todo ai ask "Move all tasks related to documentation in my Work notebook to in_progress"
|
95
|
+
```
|
96
|
+
|
97
|
+
4. Generate a JSON import file:
|
98
|
+
```bash
|
99
|
+
ruby_todo ai ask "Create a JSON file with 5 tasks for my upcoming vacation planning"
|
100
|
+
```
|
101
|
+
|
102
|
+
5. Get task statistics:
|
103
|
+
```bash
|
104
|
+
ruby_todo ai ask "Give me a summary of my Work notebook"
|
105
|
+
```
|
106
|
+
|
107
|
+
6. Search for specific tasks:
|
108
|
+
```bash
|
109
|
+
ruby_todo ai ask "Find all high priority tasks that are overdue"
|
110
|
+
```
|
111
|
+
|
112
|
+
## Troubleshooting
|
113
|
+
|
114
|
+
1. API key issues:
|
115
|
+
- Ensure your API key is correctly configured
|
116
|
+
- Try passing the API key directly: `ruby_todo ai ask "..." --api-key=your_key`
|
117
|
+
|
118
|
+
2. JSON parsing errors:
|
119
|
+
- Enable verbose mode to see the raw response: `ruby_todo ai ask "..." --verbose`
|
120
|
+
- Check if the model is generating valid JSON
|
121
|
+
|
122
|
+
3. Permission issues:
|
123
|
+
- Check file permissions on `~/.ruby_todo/ai_config.json`
|
124
|
+
|
125
|
+
4. Missing dependencies:
|
126
|
+
- Run `bundle install` to ensure all gems are installed
|
127
|
+
|
128
|
+
5. Command not found:
|
129
|
+
- Ensure the gem is properly installed: `gem list ruby_todo`
|
130
|
+
- Reinstall if necessary: `gem uninstall ruby_todo && bundle exec rake install`
|
@@ -0,0 +1,378 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module TaskStatistics
|
5
|
+
private
|
6
|
+
|
7
|
+
def handle_statistics_query(prompt)
|
8
|
+
say "\nHandling statistics query" if options[:verbose]
|
9
|
+
|
10
|
+
if prompt =~ /\b(?:show|display|get)\s+(?:me\s+)?(?:the\s+)?stats\b/i
|
11
|
+
display_task_statistics
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def display_task_statistics
|
19
|
+
notebooks = RubyTodo::Notebook.all
|
20
|
+
total_stats = { todo: 0, in_progress: 0, done: 0, archived: 0 }
|
21
|
+
|
22
|
+
notebooks.each do |notebook|
|
23
|
+
stats = notebook.task_statistics
|
24
|
+
total_stats.merge!(stats) { |_key, old_val, new_val| old_val + new_val }
|
25
|
+
|
26
|
+
display_notebook_statistics(notebook, stats)
|
27
|
+
end
|
28
|
+
|
29
|
+
display_total_statistics(total_stats)
|
30
|
+
end
|
31
|
+
|
32
|
+
def display_notebook_statistics(notebook, stats)
|
33
|
+
say "\nNotebook: #{notebook.name}".blue
|
34
|
+
say " Todo: #{stats[:todo]}".yellow
|
35
|
+
say " In Progress: #{stats[:in_progress]}".blue
|
36
|
+
say " Done: #{stats[:done]}".green
|
37
|
+
say " Archived: #{stats[:archived]}".gray
|
38
|
+
end
|
39
|
+
|
40
|
+
def display_total_statistics(stats)
|
41
|
+
say "\nTotal Statistics:".blue
|
42
|
+
say " Todo: #{stats[:todo]}".yellow
|
43
|
+
say " In Progress: #{stats[:in_progress]}".blue
|
44
|
+
say " Done: #{stats[:done]}".green
|
45
|
+
say " Archived: #{stats[:archived]}".gray
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
module TaskPriority
|
50
|
+
private
|
51
|
+
|
52
|
+
def handle_priority_query(prompt)
|
53
|
+
say "\nHandling priority query" if options[:verbose]
|
54
|
+
|
55
|
+
if prompt =~ /\b(?:show|display|get|list)\s+(?:me\s+)?(?:the\s+)?(?:high|medium|low)\s*-?\s*priority\b/i
|
56
|
+
display_priority_tasks(prompt)
|
57
|
+
return true
|
58
|
+
end
|
59
|
+
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
def display_priority_tasks(prompt)
|
64
|
+
priority = extract_priority_level(prompt)
|
65
|
+
tasks = find_priority_tasks(priority)
|
66
|
+
|
67
|
+
if tasks.any?
|
68
|
+
display_tasks_by_priority(tasks, priority)
|
69
|
+
else
|
70
|
+
say "No #{priority} priority tasks found.".yellow
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def extract_priority_level(prompt)
|
75
|
+
if prompt =~ /\b(high|medium|low)\s*-?\s*priority\b/i
|
76
|
+
Regexp.last_match(1).downcase
|
77
|
+
else
|
78
|
+
"high" # Default to high priority
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def find_priority_tasks(priority)
|
83
|
+
tasks = []
|
84
|
+
RubyTodo::Notebook.all.each do |notebook|
|
85
|
+
notebook.tasks.each do |task|
|
86
|
+
next unless task_matches_priority?(task, priority)
|
87
|
+
|
88
|
+
tasks << {
|
89
|
+
task_id: task.id,
|
90
|
+
title: task.title,
|
91
|
+
status: task.status,
|
92
|
+
notebook: notebook.name
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
tasks
|
97
|
+
end
|
98
|
+
|
99
|
+
def task_matches_priority?(task, priority)
|
100
|
+
return false unless task.tags
|
101
|
+
|
102
|
+
case priority
|
103
|
+
when "high"
|
104
|
+
task.tags.downcase.include?("high") || task.tags.downcase.include?("urgent")
|
105
|
+
when "medium"
|
106
|
+
task.tags.downcase.include?("medium") || task.tags.downcase.include?("normal")
|
107
|
+
when "low"
|
108
|
+
task.tags.downcase.include?("low")
|
109
|
+
else
|
110
|
+
false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def display_tasks_by_priority(tasks, priority)
|
115
|
+
say "\n#{priority.capitalize} Priority Tasks:".blue
|
116
|
+
tasks.each do |task|
|
117
|
+
status_color = case task[:status]
|
118
|
+
when "todo" then :yellow
|
119
|
+
when "in_progress" then :blue
|
120
|
+
when "done" then :green
|
121
|
+
else :white
|
122
|
+
end
|
123
|
+
|
124
|
+
say " [#{task[:notebook]}] Task #{task[:task_id]}: #{task[:title]}".send(status_color)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
module TaskDeadlines
|
130
|
+
private
|
131
|
+
|
132
|
+
def handle_deadline_query(prompt)
|
133
|
+
say "\nHandling deadline query" if options[:verbose]
|
134
|
+
# rubocop:disable Layout/LineLength
|
135
|
+
deadline_pattern = /\b(?:show|display|get|list)\s+(?:me\s+)?(?:the\s+)?(?:upcoming|due|overdue)\s+(?:tasks|deadlines)\b/
|
136
|
+
# rubocop:enable Layout/LineLength
|
137
|
+
if prompt =~ /#{deadline_pattern}/i
|
138
|
+
display_deadline_tasks(prompt)
|
139
|
+
return true
|
140
|
+
end
|
141
|
+
|
142
|
+
false
|
143
|
+
end
|
144
|
+
|
145
|
+
def display_deadline_tasks(prompt)
|
146
|
+
deadline_type = extract_deadline_type(prompt)
|
147
|
+
tasks = find_deadline_tasks(deadline_type)
|
148
|
+
|
149
|
+
if tasks.any?
|
150
|
+
display_tasks_by_deadline(tasks, deadline_type)
|
151
|
+
else
|
152
|
+
say "No #{deadline_type} tasks found.".yellow
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def extract_deadline_type(prompt)
|
157
|
+
if prompt =~ /\b(upcoming|due|overdue)\b/i
|
158
|
+
Regexp.last_match(1).downcase
|
159
|
+
else
|
160
|
+
"upcoming" # Default to upcoming
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def find_deadline_tasks(deadline_type)
|
165
|
+
tasks = []
|
166
|
+
RubyTodo::Notebook.all.each do |notebook|
|
167
|
+
notebook.tasks.each do |task|
|
168
|
+
next unless task_matches_deadline?(task, deadline_type)
|
169
|
+
|
170
|
+
tasks << {
|
171
|
+
task_id: task.id,
|
172
|
+
title: task.title,
|
173
|
+
status: task.status,
|
174
|
+
notebook: notebook.name,
|
175
|
+
deadline: task.deadline
|
176
|
+
}
|
177
|
+
end
|
178
|
+
end
|
179
|
+
tasks
|
180
|
+
end
|
181
|
+
|
182
|
+
def task_matches_deadline?(task, deadline_type)
|
183
|
+
return false unless task.deadline
|
184
|
+
|
185
|
+
case deadline_type
|
186
|
+
when "upcoming"
|
187
|
+
task.deadline > Time.now && task.deadline <= Time.now + (7 * 24 * 60 * 60) # 7 days
|
188
|
+
when "due"
|
189
|
+
task.deadline <= Time.now + (24 * 60 * 60) # 1 day
|
190
|
+
when "overdue"
|
191
|
+
task.deadline < Time.now
|
192
|
+
else
|
193
|
+
false
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def display_tasks_by_deadline(tasks, deadline_type)
|
198
|
+
say "\n#{deadline_type.capitalize} Tasks:".blue
|
199
|
+
tasks.each do |task|
|
200
|
+
deadline_str = task[:deadline].strftime("%Y-%m-%d %H:%M")
|
201
|
+
status_color = case task[:status]
|
202
|
+
when "todo" then :yellow
|
203
|
+
when "in_progress" then :blue
|
204
|
+
when "done" then :green
|
205
|
+
else :white
|
206
|
+
end
|
207
|
+
|
208
|
+
say " [#{task[:notebook]}] Task #{task[:task_id]}: " \
|
209
|
+
"#{task[:title]} (Due: #{deadline_str})".send(status_color)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
module TaskCreation
|
215
|
+
private
|
216
|
+
|
217
|
+
def handle_task_creation(prompt, prompt_lower)
|
218
|
+
say "\n=== Detecting task creation request ===" if options[:verbose]
|
219
|
+
|
220
|
+
title = extract_task_title(prompt)
|
221
|
+
return false unless title
|
222
|
+
|
223
|
+
notebook_name = determine_notebook_name(prompt_lower)
|
224
|
+
return false unless notebook_name
|
225
|
+
|
226
|
+
priority = determine_priority(prompt_lower)
|
227
|
+
|
228
|
+
create_task(notebook_name, title, priority)
|
229
|
+
true
|
230
|
+
end
|
231
|
+
|
232
|
+
def extract_task_title(prompt)
|
233
|
+
# Try to extract title from quotes first
|
234
|
+
title_match = prompt.match(/'([^']+)'|"([^"]+)"/)
|
235
|
+
|
236
|
+
if title_match
|
237
|
+
title_match[1] || title_match[2]
|
238
|
+
else
|
239
|
+
# If no quoted title found, try extracting from the prompt
|
240
|
+
extract_title_from_text(prompt)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def extract_title_from_text(prompt)
|
245
|
+
potential_title = prompt
|
246
|
+
phrases_to_remove = [
|
247
|
+
"create a task", "create task", "add a task", "add task",
|
248
|
+
"called", "named", "with", "priority", "high", "medium", "low",
|
249
|
+
"in", "notebook"
|
250
|
+
]
|
251
|
+
|
252
|
+
phrases_to_remove.each do |phrase|
|
253
|
+
potential_title = potential_title.gsub(/#{phrase}/i, " ")
|
254
|
+
end
|
255
|
+
|
256
|
+
result = potential_title.strip
|
257
|
+
result.empty? ? nil : result
|
258
|
+
end
|
259
|
+
|
260
|
+
def determine_notebook_name(prompt_lower)
|
261
|
+
return nil unless Notebook.default_notebook
|
262
|
+
|
263
|
+
notebook_name = Notebook.default_notebook.name
|
264
|
+
|
265
|
+
# Try to extract a specific notebook name from the prompt
|
266
|
+
Notebook.all.each do |notebook|
|
267
|
+
if prompt_lower.include?(notebook.name.downcase)
|
268
|
+
notebook_name = notebook.name
|
269
|
+
break
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
notebook_name
|
274
|
+
end
|
275
|
+
|
276
|
+
def determine_priority(prompt_lower)
|
277
|
+
if prompt_lower.include?("high priority") || prompt_lower.match(/priority.*high/)
|
278
|
+
"high"
|
279
|
+
elsif prompt_lower.include?("medium priority") || prompt_lower.match(/priority.*medium/)
|
280
|
+
"medium"
|
281
|
+
elsif prompt_lower.include?("low priority") || prompt_lower.match(/priority.*low/)
|
282
|
+
"low"
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def create_task(notebook_name, title, priority)
|
287
|
+
say "\nCreating task in notebook: #{notebook_name}" if options[:verbose]
|
288
|
+
cli_args = ["task:add", notebook_name, title]
|
289
|
+
|
290
|
+
# Add priority if specified
|
291
|
+
cli_args.push("--priority", priority) if priority
|
292
|
+
|
293
|
+
RubyTodo::CLI.start(cli_args)
|
294
|
+
|
295
|
+
# Create a simple explanation
|
296
|
+
priority_text = priority ? " with #{priority} priority" : ""
|
297
|
+
say "\nCreated task '#{title}'#{priority_text} in the #{notebook_name} notebook"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
module CommonQueryHandler
|
302
|
+
include TaskStatistics
|
303
|
+
include TaskPriority
|
304
|
+
include TaskDeadlines
|
305
|
+
include TaskCreation
|
306
|
+
|
307
|
+
def handle_common_query(prompt)
|
308
|
+
handle_statistics_query(prompt) ||
|
309
|
+
handle_priority_query(prompt) ||
|
310
|
+
handle_deadline_query(prompt)
|
311
|
+
end
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
def high_priority_query?(prompt_lower)
|
316
|
+
prompt_lower.include?("high priority") ||
|
317
|
+
(prompt_lower.include?("priority") && prompt_lower.include?("high"))
|
318
|
+
end
|
319
|
+
|
320
|
+
def medium_priority_query?(prompt_lower)
|
321
|
+
prompt_lower.include?("medium priority") ||
|
322
|
+
(prompt_lower.include?("priority") && prompt_lower.include?("medium"))
|
323
|
+
end
|
324
|
+
|
325
|
+
def statistics_query?(prompt_lower)
|
326
|
+
(prompt_lower.include?("statistics") || prompt_lower.include?("stats")) &&
|
327
|
+
(prompt_lower.include?("notebook") || prompt_lower.include?("tasks"))
|
328
|
+
end
|
329
|
+
|
330
|
+
def status_tasks_query?(prompt_lower)
|
331
|
+
statuses = { "todo" => "todo", "in progress" => "in_progress", "done" => "done", "archived" => "archived" }
|
332
|
+
statuses.keys.any? { |status| prompt_lower.include?(status) }
|
333
|
+
end
|
334
|
+
|
335
|
+
def notebook_listing_query?(prompt_lower)
|
336
|
+
prompt_lower.include?("list notebooks") ||
|
337
|
+
prompt_lower.include?("show notebooks") ||
|
338
|
+
prompt_lower.include?("display notebooks")
|
339
|
+
end
|
340
|
+
|
341
|
+
def handle_status_tasks(prompt_lower)
|
342
|
+
say "\n=== Detecting status tasks request ===" if options[:verbose]
|
343
|
+
|
344
|
+
statuses = { "todo" => "todo", "in progress" => "in_progress", "done" => "done", "archived" => "archived" }
|
345
|
+
status = nil
|
346
|
+
|
347
|
+
statuses.each do |name, value|
|
348
|
+
if prompt_lower.include?(name)
|
349
|
+
status = value
|
350
|
+
break
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
return false unless status
|
355
|
+
|
356
|
+
tasks = find_tasks_by_status(status)
|
357
|
+
display_tasks_by_status(tasks, status)
|
358
|
+
true
|
359
|
+
end
|
360
|
+
|
361
|
+
def handle_notebook_listing(_prompt_lower)
|
362
|
+
say "\n=== Detecting notebook listing request ===" if options[:verbose]
|
363
|
+
|
364
|
+
notebooks = Notebook.all
|
365
|
+
if notebooks.empty?
|
366
|
+
say "No notebooks found.".yellow
|
367
|
+
return true
|
368
|
+
end
|
369
|
+
|
370
|
+
say "\nNotebooks:".blue
|
371
|
+
notebooks.each do |notebook|
|
372
|
+
say " #{notebook.name}".green
|
373
|
+
end
|
374
|
+
|
375
|
+
true
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyTodo
|
4
|
+
module ConfigurationManagement
|
5
|
+
def load_api_key_from_config
|
6
|
+
config = load_config
|
7
|
+
config["openai"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def load_config
|
11
|
+
return {} unless File.exist?(config_file)
|
12
|
+
|
13
|
+
YAML.load_file(config_file) || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def save_config(key, value)
|
17
|
+
config = load_config
|
18
|
+
config[key] = value
|
19
|
+
FileUtils.mkdir_p(File.dirname(config_file))
|
20
|
+
File.write(config_file, config.to_yaml)
|
21
|
+
end
|
22
|
+
|
23
|
+
def config_file
|
24
|
+
File.join(Dir.home, ".config", "ruby_todo", "config.yml")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|