hiiro 0.1.68 → 0.1.69
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/bin/h-todo +273 -0
- data/lib/hiiro/todo.rb +337 -0
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +5 -0
- data/plugins/tasks.rb +120 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70e0ff0419923a10f71567fafbb03fed3c13a31bd3b338acb018cfc9f3dce8ec
|
|
4
|
+
data.tar.gz: 4d20bf045c56dde0ec03fdd9b7d47b24cb73aba90cf1871c36ffd206fce46b61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29229cc49ada8ecf12bc34075818a4300a7a1b9e88729893e7216c85023b3c0c4a79c3f13dc8218d35b8516724c7d730a35ba04707bf08165117265125046acd
|
|
7
|
+
data.tar.gz: 001d4cb6ba6f1c0bb912efb2fe952462136315a6358e61be0a5d8a322430d62b60ce1df22204cbd6af57d031e0f367c42a71d4d2972db732853f082886f307b6
|
data/bin/h-todo
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'hiiro'
|
|
4
|
+
|
|
5
|
+
tm = Hiiro::TodoManager.new
|
|
6
|
+
o = Hiiro.init(*ARGV, todo_file: tm.todo_file)
|
|
7
|
+
|
|
8
|
+
o.add_subcmd(:ls) do |*args|
|
|
9
|
+
show_all = args.delete('-a') || args.delete('--all')
|
|
10
|
+
status_filter = nil
|
|
11
|
+
tag_filter = nil
|
|
12
|
+
task_filter = nil
|
|
13
|
+
|
|
14
|
+
while args.any?
|
|
15
|
+
arg = args.shift
|
|
16
|
+
case arg
|
|
17
|
+
when '-s', '--status'
|
|
18
|
+
status_filter = args.shift
|
|
19
|
+
when '-t', '--tag'
|
|
20
|
+
tag_filter = args.shift
|
|
21
|
+
when '--task'
|
|
22
|
+
task_filter = args.shift
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
items = if status_filter
|
|
27
|
+
tm.filter_by_status(*status_filter.split(','))
|
|
28
|
+
elsif tag_filter
|
|
29
|
+
tm.filter_by_tag(tag_filter)
|
|
30
|
+
elsif task_filter
|
|
31
|
+
tm.filter_by_task(task_filter)
|
|
32
|
+
elsif show_all
|
|
33
|
+
tm.all
|
|
34
|
+
else
|
|
35
|
+
tm.active
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if items.empty?
|
|
39
|
+
puts "No todo items found."
|
|
40
|
+
else
|
|
41
|
+
puts tm.list(items)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
o.add_subcmd(:list) do |*args|
|
|
46
|
+
o.run_subcmd(:ls, *args)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
o.add_subcmd(:add) do |*args|
|
|
50
|
+
if args.empty?
|
|
51
|
+
new_items = tm.edit_items
|
|
52
|
+
if new_items.empty?
|
|
53
|
+
puts "No items added."
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
tm.add_items(new_items)
|
|
58
|
+
if new_items.length == 1
|
|
59
|
+
puts "Added: #{tm.format_item(new_items.first)}"
|
|
60
|
+
else
|
|
61
|
+
puts "Added #{new_items.length} items:"
|
|
62
|
+
new_items.each { |item| puts " #{tm.format_item(item)}" }
|
|
63
|
+
end
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
tags = nil
|
|
68
|
+
text_parts = []
|
|
69
|
+
|
|
70
|
+
while args.any?
|
|
71
|
+
arg = args.shift
|
|
72
|
+
case arg
|
|
73
|
+
when '-t', '--tags'
|
|
74
|
+
tags = args.shift
|
|
75
|
+
else
|
|
76
|
+
text_parts << arg
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
text = text_parts.join(' ')
|
|
81
|
+
if text.empty?
|
|
82
|
+
puts "Usage: h todo add <text> [-t tags]"
|
|
83
|
+
exit 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
item = tm.add(text, tags: tags)
|
|
87
|
+
puts "Added: #{tm.format_item(item)}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
o.add_subcmd(:rm) do |*args|
|
|
91
|
+
id_or_index = args.shift
|
|
92
|
+
if id_or_index.nil?
|
|
93
|
+
puts "Usage: h todo rm <id|index>"
|
|
94
|
+
exit 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
item = tm.remove(id_or_index)
|
|
98
|
+
if item
|
|
99
|
+
puts "Removed: #{item.text}"
|
|
100
|
+
else
|
|
101
|
+
puts "Item not found: #{id_or_index}"
|
|
102
|
+
exit 1
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
o.add_subcmd(:remove) do |*args|
|
|
107
|
+
o.run_subcmd(:rm, *args)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
o.add_subcmd(:change) do |*args|
|
|
111
|
+
id_or_index = args.shift
|
|
112
|
+
if id_or_index.nil?
|
|
113
|
+
puts "Usage: h todo change <id|index> [--text TEXT] [--tags TAGS] [--status STATUS]"
|
|
114
|
+
exit 1
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
text = nil
|
|
118
|
+
tags = nil
|
|
119
|
+
status = nil
|
|
120
|
+
|
|
121
|
+
while args.any?
|
|
122
|
+
arg = args.shift
|
|
123
|
+
case arg
|
|
124
|
+
when '--text'
|
|
125
|
+
text = args.shift
|
|
126
|
+
when '--tags', '-t'
|
|
127
|
+
tags = args.shift
|
|
128
|
+
when '--status', '-s'
|
|
129
|
+
status = args.shift
|
|
130
|
+
else
|
|
131
|
+
text ||= ''
|
|
132
|
+
text = [text, arg, *args].join(' ').strip
|
|
133
|
+
break
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
item = tm.change(id_or_index, text: text, tags: tags, status: status)
|
|
138
|
+
if item
|
|
139
|
+
puts "Updated: #{tm.format_item(item)}"
|
|
140
|
+
else
|
|
141
|
+
puts "Item not found: #{id_or_index}"
|
|
142
|
+
exit 1
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
o.add_subcmd(:start) do |*args|
|
|
147
|
+
id_or_index = args.shift
|
|
148
|
+
if id_or_index.nil?
|
|
149
|
+
puts "Usage: h todo start <id|index>"
|
|
150
|
+
exit 1
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
item = tm.start(id_or_index)
|
|
154
|
+
if item
|
|
155
|
+
puts "Started: #{tm.format_item(item)}"
|
|
156
|
+
else
|
|
157
|
+
puts "Item not found: #{id_or_index}"
|
|
158
|
+
exit 1
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
o.add_subcmd(:done) do |*args|
|
|
163
|
+
id_or_index = args.shift
|
|
164
|
+
if id_or_index.nil?
|
|
165
|
+
puts "Usage: h todo done <id|index>"
|
|
166
|
+
exit 1
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
item = tm.done(id_or_index)
|
|
170
|
+
if item
|
|
171
|
+
puts "Done: #{tm.format_item(item)}"
|
|
172
|
+
else
|
|
173
|
+
puts "Item not found: #{id_or_index}"
|
|
174
|
+
exit 1
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
o.add_subcmd(:skip) do |*args|
|
|
179
|
+
id_or_index = args.shift
|
|
180
|
+
if id_or_index.nil?
|
|
181
|
+
puts "Usage: h todo skip <id|index>"
|
|
182
|
+
exit 1
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
item = tm.skip(id_or_index)
|
|
186
|
+
if item
|
|
187
|
+
puts "Skipped: #{tm.format_item(item)}"
|
|
188
|
+
else
|
|
189
|
+
puts "Item not found: #{id_or_index}"
|
|
190
|
+
exit 1
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
o.add_subcmd(:reset) do |*args|
|
|
195
|
+
id_or_index = args.shift
|
|
196
|
+
if id_or_index.nil?
|
|
197
|
+
puts "Usage: h todo reset <id|index>"
|
|
198
|
+
exit 1
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
item = tm.reset(id_or_index)
|
|
202
|
+
if item
|
|
203
|
+
puts "Reset: #{tm.format_item(item)}"
|
|
204
|
+
else
|
|
205
|
+
puts "Item not found: #{id_or_index}"
|
|
206
|
+
exit 1
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
o.add_subcmd(:search) do |*args|
|
|
211
|
+
query = args.join(' ')
|
|
212
|
+
if query.empty?
|
|
213
|
+
puts "Usage: h todo search <query>"
|
|
214
|
+
exit 1
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
items = tm.search(query)
|
|
218
|
+
if items.empty?
|
|
219
|
+
puts "No items matching: #{query}"
|
|
220
|
+
else
|
|
221
|
+
puts tm.list(items)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
o.add_subcmd(:path) do |*args|
|
|
226
|
+
print tm.todo_file
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
o.add_subcmd(:editall) do |*args|
|
|
230
|
+
editor = ENV['EDITOR'] || 'safe_nvim' || 'nvim'
|
|
231
|
+
system(editor, tm.todo_file)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
o.add_subcmd(:help) do |*args|
|
|
235
|
+
puts <<~HELP
|
|
236
|
+
Usage: h todo <command> [args]
|
|
237
|
+
|
|
238
|
+
Commands:
|
|
239
|
+
ls, list List todo items (active by default)
|
|
240
|
+
-a, --all Show all items including done/skip
|
|
241
|
+
-s, --status STATUS Filter by status (not_started,started,done,skip)
|
|
242
|
+
-t, --tag TAG Filter by tag
|
|
243
|
+
--task TASK Filter by task name
|
|
244
|
+
|
|
245
|
+
add [text] [-t tags] Add todo item(s)
|
|
246
|
+
With no args, opens editor for YAML input
|
|
247
|
+
|
|
248
|
+
rm, remove <id|index> Remove a todo item
|
|
249
|
+
|
|
250
|
+
change <id|index> Modify a todo item
|
|
251
|
+
--text TEXT New text
|
|
252
|
+
--tags TAGS New tags
|
|
253
|
+
--status STATUS New status
|
|
254
|
+
|
|
255
|
+
start <id|index> Mark item as started
|
|
256
|
+
done <id|index> Mark item as done
|
|
257
|
+
skip <id|index> Mark item as skipped
|
|
258
|
+
reset <id|index> Reset item to not_started
|
|
259
|
+
|
|
260
|
+
search <query> Search items by text, tags, or task
|
|
261
|
+
|
|
262
|
+
path Print path to todo.yml
|
|
263
|
+
editall Open todo.yml in editor
|
|
264
|
+
|
|
265
|
+
Status icons:
|
|
266
|
+
[ ] not_started
|
|
267
|
+
[>] started
|
|
268
|
+
[x] done
|
|
269
|
+
[-] skip
|
|
270
|
+
HELP
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
o.run
|
data/lib/hiiro/todo.rb
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module Hiiro
|
|
7
|
+
class TodoItem
|
|
8
|
+
STATUSES = %w[not_started started done skip].freeze
|
|
9
|
+
|
|
10
|
+
attr_accessor :id, :text, :status, :tags
|
|
11
|
+
attr_accessor :task_name, :subtask_name, :tree, :branch, :session
|
|
12
|
+
attr_accessor :created_at, :updated_at
|
|
13
|
+
|
|
14
|
+
def initialize(
|
|
15
|
+
id: nil,
|
|
16
|
+
text:,
|
|
17
|
+
status: 'not_started',
|
|
18
|
+
tags: nil,
|
|
19
|
+
task_name: nil,
|
|
20
|
+
subtask_name: nil,
|
|
21
|
+
tree: nil,
|
|
22
|
+
branch: nil,
|
|
23
|
+
session: nil,
|
|
24
|
+
created_at: nil,
|
|
25
|
+
updated_at: nil
|
|
26
|
+
)
|
|
27
|
+
@id = id || SecureRandom.hex(4)
|
|
28
|
+
@text = text
|
|
29
|
+
@status = STATUSES.include?(status) ? status : 'not_started'
|
|
30
|
+
@tags = tags
|
|
31
|
+
@task_name = task_name
|
|
32
|
+
@subtask_name = subtask_name
|
|
33
|
+
@tree = tree
|
|
34
|
+
@branch = branch
|
|
35
|
+
@session = session
|
|
36
|
+
@created_at = created_at || Time.now.to_s
|
|
37
|
+
@updated_at = updated_at || @created_at
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def tags_list
|
|
41
|
+
return [] if tags.nil? || tags.empty?
|
|
42
|
+
tags.split(',').map(&:strip)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def has_tag?(tag)
|
|
46
|
+
tags_list.any? { |t| t.downcase == tag.downcase }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def add_tag(tag)
|
|
50
|
+
current = tags_list
|
|
51
|
+
return if current.any? { |t| t.downcase == tag.downcase }
|
|
52
|
+
current << tag
|
|
53
|
+
@tags = current.join(', ')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def remove_tag(tag)
|
|
57
|
+
current = tags_list.reject { |t| t.downcase == tag.downcase }
|
|
58
|
+
@tags = current.empty? ? nil : current.join(', ')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def has_task_info?
|
|
62
|
+
!task_name.nil? || !subtask_name.nil?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def full_task_name
|
|
66
|
+
return nil unless has_task_info?
|
|
67
|
+
subtask_name ? "#{task_name}/#{subtask_name}" : task_name
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def update_status(new_status)
|
|
71
|
+
return false unless STATUSES.include?(new_status)
|
|
72
|
+
@status = new_status
|
|
73
|
+
@updated_at = Time.now.to_s
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def to_h
|
|
78
|
+
h = {
|
|
79
|
+
'id' => id,
|
|
80
|
+
'text' => text,
|
|
81
|
+
'status' => status,
|
|
82
|
+
'created_at' => created_at,
|
|
83
|
+
'updated_at' => updated_at
|
|
84
|
+
}
|
|
85
|
+
h['tags'] = tags if tags && !tags.empty?
|
|
86
|
+
h['task_name'] = task_name if task_name
|
|
87
|
+
h['subtask_name'] = subtask_name if subtask_name
|
|
88
|
+
h['tree'] = tree if tree
|
|
89
|
+
h['branch'] = branch if branch
|
|
90
|
+
h['session'] = session if session
|
|
91
|
+
h
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.from_h(h)
|
|
95
|
+
new(
|
|
96
|
+
id: h['id'],
|
|
97
|
+
text: h['text'],
|
|
98
|
+
status: h['status'],
|
|
99
|
+
tags: h['tags'],
|
|
100
|
+
task_name: h['task_name'],
|
|
101
|
+
subtask_name: h['subtask_name'],
|
|
102
|
+
tree: h['tree'],
|
|
103
|
+
branch: h['branch'],
|
|
104
|
+
session: h['session'],
|
|
105
|
+
created_at: h['created_at'],
|
|
106
|
+
updated_at: h['updated_at']
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def match?(query)
|
|
111
|
+
query = query.downcase
|
|
112
|
+
text.downcase.include?(query) ||
|
|
113
|
+
tags_list.any? { |t| t.downcase.include?(query) } ||
|
|
114
|
+
(task_name && task_name.downcase.include?(query)) ||
|
|
115
|
+
(subtask_name && subtask_name.downcase.include?(query)) ||
|
|
116
|
+
(tree && tree.downcase.include?(query)) ||
|
|
117
|
+
(branch && branch.downcase.include?(query))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class TodoManager
|
|
122
|
+
TODO_FILE = File.join(Dir.home, '.config', 'hiiro', 'todo.yml')
|
|
123
|
+
|
|
124
|
+
ITEM_TEMPLATE = {
|
|
125
|
+
'text' => '',
|
|
126
|
+
'status' => 'not_started',
|
|
127
|
+
'tags' => nil
|
|
128
|
+
}.freeze
|
|
129
|
+
|
|
130
|
+
attr_reader :items, :todo_file
|
|
131
|
+
|
|
132
|
+
def initialize(file_path: nil)
|
|
133
|
+
@todo_file = file_path || TODO_FILE
|
|
134
|
+
@file_path = @todo_file
|
|
135
|
+
@items = load_items
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# --- High-level API ---
|
|
139
|
+
|
|
140
|
+
def all
|
|
141
|
+
items
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def find(id)
|
|
145
|
+
items.find { |item| item.id == id || item.id.start_with?(id) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def find_by_index(index)
|
|
149
|
+
items[index.to_i]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def add(text, tags: nil, task_info: nil)
|
|
153
|
+
item = TodoItem.new(
|
|
154
|
+
text: text,
|
|
155
|
+
tags: tags,
|
|
156
|
+
task_name: task_info&.dig(:task_name),
|
|
157
|
+
subtask_name: task_info&.dig(:subtask_name),
|
|
158
|
+
tree: task_info&.dig(:tree),
|
|
159
|
+
branch: task_info&.dig(:branch),
|
|
160
|
+
session: task_info&.dig(:session)
|
|
161
|
+
)
|
|
162
|
+
@items << item
|
|
163
|
+
save
|
|
164
|
+
item
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def edit_items(items_to_edit = nil, task_info: nil)
|
|
168
|
+
items_array = if items_to_edit.nil?
|
|
169
|
+
[ITEM_TEMPLATE.dup]
|
|
170
|
+
elsif items_to_edit.is_a?(Array)
|
|
171
|
+
items_to_edit.map { |item| item.is_a?(TodoItem) ? editable_hash(item) : item }
|
|
172
|
+
else
|
|
173
|
+
[items_to_edit.is_a?(TodoItem) ? editable_hash(items_to_edit) : items_to_edit]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
tmpfile = Tempfile.new(['todo-edit-', '.yml'])
|
|
177
|
+
tmpfile.write(items_array.to_yaml)
|
|
178
|
+
tmpfile.close
|
|
179
|
+
|
|
180
|
+
editor = ENV['EDITOR'] || 'safe_nvim' || 'nvim'
|
|
181
|
+
system(editor, tmpfile.path)
|
|
182
|
+
|
|
183
|
+
updated_data = YAML.safe_load_file(tmpfile.path)
|
|
184
|
+
tmpfile.unlink
|
|
185
|
+
|
|
186
|
+
return [] if updated_data.nil?
|
|
187
|
+
|
|
188
|
+
updated_array = updated_data.is_a?(Array) ? updated_data : [updated_data]
|
|
189
|
+
updated_array.filter_map do |h|
|
|
190
|
+
next if h['text'].nil? || h['text'].to_s.strip.empty?
|
|
191
|
+
TodoItem.new(
|
|
192
|
+
text: h['text'],
|
|
193
|
+
status: h['status'] || 'not_started',
|
|
194
|
+
tags: h['tags'],
|
|
195
|
+
task_name: task_info&.dig(:task_name),
|
|
196
|
+
subtask_name: task_info&.dig(:subtask_name),
|
|
197
|
+
tree: task_info&.dig(:tree),
|
|
198
|
+
branch: task_info&.dig(:branch),
|
|
199
|
+
session: task_info&.dig(:session)
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def add_items(new_items)
|
|
205
|
+
@items.concat(new_items)
|
|
206
|
+
save
|
|
207
|
+
new_items
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def remove(id_or_index)
|
|
211
|
+
item = resolve_item(id_or_index)
|
|
212
|
+
return nil unless item
|
|
213
|
+
@items.delete(item)
|
|
214
|
+
save
|
|
215
|
+
item
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def change(id_or_index, text: nil, tags: nil, status: nil)
|
|
219
|
+
item = resolve_item(id_or_index)
|
|
220
|
+
return nil unless item
|
|
221
|
+
|
|
222
|
+
item.text = text if text
|
|
223
|
+
item.tags = tags if tags
|
|
224
|
+
item.update_status(status) if status
|
|
225
|
+
item.updated_at = Time.now.to_s
|
|
226
|
+
save
|
|
227
|
+
item
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def start(id_or_index)
|
|
231
|
+
change_status(id_or_index, 'started')
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def done(id_or_index)
|
|
235
|
+
change_status(id_or_index, 'done')
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def skip(id_or_index)
|
|
239
|
+
change_status(id_or_index, 'skip')
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def reset(id_or_index)
|
|
243
|
+
change_status(id_or_index, 'not_started')
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def search(query)
|
|
247
|
+
items.select { |item| item.match?(query) }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def filter_by_status(*statuses)
|
|
251
|
+
items.select { |item| statuses.include?(item.status) }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def filter_by_tag(tag)
|
|
255
|
+
items.select { |item| item.has_tag?(tag) }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def filter_by_task(task_name)
|
|
259
|
+
items.select { |item| item.task_name == task_name || item.full_task_name == task_name }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def active
|
|
263
|
+
filter_by_status('not_started', 'started')
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def completed
|
|
267
|
+
filter_by_status('done', 'skip')
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# --- List display ---
|
|
271
|
+
|
|
272
|
+
def list(items_to_show = nil, show_all: false)
|
|
273
|
+
items_to_show ||= show_all ? all : active
|
|
274
|
+
return "No todo items found." if items_to_show.empty?
|
|
275
|
+
|
|
276
|
+
lines = []
|
|
277
|
+
items_to_show.each_with_index do |item, idx|
|
|
278
|
+
lines << format_item(item, idx)
|
|
279
|
+
end
|
|
280
|
+
lines.join("\n")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def format_item(item, index = nil)
|
|
284
|
+
status_icon = case item.status
|
|
285
|
+
when 'not_started' then '[ ]'
|
|
286
|
+
when 'started' then '[>]'
|
|
287
|
+
when 'done' then '[x]'
|
|
288
|
+
when 'skip' then '[-]'
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
idx_str = index ? format('%2d.', index) : ' '
|
|
292
|
+
line = "#{idx_str} #{status_icon} #{item.text}"
|
|
293
|
+
line += " [#{item.tags}]" if item.tags && !item.tags.empty?
|
|
294
|
+
line += " (#{item.full_task_name})" if item.has_task_info?
|
|
295
|
+
line
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
private
|
|
299
|
+
|
|
300
|
+
def resolve_item(id_or_index)
|
|
301
|
+
if id_or_index.is_a?(Integer) || id_or_index =~ /^\d+$/
|
|
302
|
+
find_by_index(id_or_index.to_i)
|
|
303
|
+
else
|
|
304
|
+
find(id_or_index)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def change_status(id_or_index, status)
|
|
309
|
+
item = resolve_item(id_or_index)
|
|
310
|
+
return nil unless item
|
|
311
|
+
item.update_status(status)
|
|
312
|
+
save
|
|
313
|
+
item
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def editable_hash(item)
|
|
317
|
+
{
|
|
318
|
+
'text' => item.text,
|
|
319
|
+
'status' => item.status,
|
|
320
|
+
'tags' => item.tags
|
|
321
|
+
}
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def load_items
|
|
325
|
+
return [] unless File.exist?(@file_path)
|
|
326
|
+
|
|
327
|
+
data = YAML.safe_load_file(@file_path) || {}
|
|
328
|
+
(data['todos'] || []).map { |h| TodoItem.from_h(h) }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def save
|
|
332
|
+
FileUtils.mkdir_p(File.dirname(@file_path))
|
|
333
|
+
data = { 'todos' => items.map(&:to_h) }
|
|
334
|
+
File.write(@file_path, YAML.dump(data))
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
data/lib/hiiro/version.rb
CHANGED
data/lib/hiiro.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "hiiro/git"
|
|
|
9
9
|
require_relative "hiiro/history"
|
|
10
10
|
require_relative "hiiro/options"
|
|
11
11
|
require_relative "hiiro/sk"
|
|
12
|
+
require_relative "hiiro/todo"
|
|
12
13
|
|
|
13
14
|
class String
|
|
14
15
|
def underscore(camel_cased_word=self)
|
|
@@ -98,6 +99,10 @@ class Hiiro
|
|
|
98
99
|
Hiiro.init(bin_name:, args:, **kwargs, &block)
|
|
99
100
|
end
|
|
100
101
|
|
|
102
|
+
def todo_manager
|
|
103
|
+
@todo_manager ||= TodoManager.new
|
|
104
|
+
end
|
|
105
|
+
|
|
101
106
|
def history
|
|
102
107
|
@history ||= History.new
|
|
103
108
|
end
|
data/plugins/tasks.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'yaml'
|
|
2
2
|
require 'fileutils'
|
|
3
|
+
require_relative '../lib/hiiro/todo'
|
|
3
4
|
|
|
4
5
|
WORK_DIR = File.join(Dir.home, 'work')
|
|
5
6
|
REPO_PATH = File.join(WORK_DIR, '.bare')
|
|
@@ -923,6 +924,125 @@ module Tasks
|
|
|
923
924
|
h.add_subcmd(:edit) do
|
|
924
925
|
system(ENV['EDITOR'] || 'nvim', __FILE__)
|
|
925
926
|
end
|
|
927
|
+
|
|
928
|
+
h.add_subcmd(:todo) do |*todo_args|
|
|
929
|
+
todo_manager = Hiiro::TodoManager.new
|
|
930
|
+
task = tm.current_task
|
|
931
|
+
|
|
932
|
+
task_info = if task
|
|
933
|
+
{
|
|
934
|
+
task_name: task.subtask? ? task.parent_name : task.name,
|
|
935
|
+
subtask_name: task.subtask? ? task.short_name : nil,
|
|
936
|
+
tree: task.tree_name,
|
|
937
|
+
branch: task.branch,
|
|
938
|
+
session: task.session_name
|
|
939
|
+
}
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
todo_subcmd = todo_args.shift
|
|
943
|
+
case todo_subcmd
|
|
944
|
+
when 'ls', 'list', nil
|
|
945
|
+
show_all = todo_args.delete('-a') || todo_args.delete('--all')
|
|
946
|
+
# Default to filtering by current task unless -a or --all is used
|
|
947
|
+
items = if show_all
|
|
948
|
+
todo_manager.all
|
|
949
|
+
elsif task
|
|
950
|
+
todo_manager.filter_by_task(task.name).select { |i| %w[not_started started].include?(i.status) }
|
|
951
|
+
else
|
|
952
|
+
todo_manager.active
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
if items.empty?
|
|
956
|
+
puts task ? "No todo items for task '#{task.name}'." : "No todo items found."
|
|
957
|
+
else
|
|
958
|
+
puts todo_manager.list(items)
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
when 'add'
|
|
962
|
+
if todo_args.empty?
|
|
963
|
+
new_items = todo_manager.edit_items(task_info: task_info)
|
|
964
|
+
if new_items.empty?
|
|
965
|
+
puts "No items added."
|
|
966
|
+
next
|
|
967
|
+
end
|
|
968
|
+
todo_manager.add_items(new_items)
|
|
969
|
+
if new_items.length == 1
|
|
970
|
+
puts "Added: #{todo_manager.format_item(new_items.first)}"
|
|
971
|
+
else
|
|
972
|
+
puts "Added #{new_items.length} items:"
|
|
973
|
+
new_items.each { |item| puts " #{todo_manager.format_item(item)}" }
|
|
974
|
+
end
|
|
975
|
+
next
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
tags = nil
|
|
979
|
+
text_parts = []
|
|
980
|
+
while todo_args.any?
|
|
981
|
+
arg = todo_args.shift
|
|
982
|
+
case arg
|
|
983
|
+
when '-t', '--tags'
|
|
984
|
+
tags = todo_args.shift
|
|
985
|
+
else
|
|
986
|
+
text_parts << arg
|
|
987
|
+
end
|
|
988
|
+
end
|
|
989
|
+
text = text_parts.join(' ')
|
|
990
|
+
item = todo_manager.add(text, tags: tags, task_info: task_info)
|
|
991
|
+
puts "Added: #{todo_manager.format_item(item)}"
|
|
992
|
+
|
|
993
|
+
when 'rm', 'remove'
|
|
994
|
+
id_or_index = todo_args.shift
|
|
995
|
+
unless id_or_index
|
|
996
|
+
puts "Usage: h #{tm.scope} todo rm <id|index>"
|
|
997
|
+
next
|
|
998
|
+
end
|
|
999
|
+
item = todo_manager.remove(id_or_index)
|
|
1000
|
+
puts item ? "Removed: #{item.text}" : "Item not found: #{id_or_index}"
|
|
1001
|
+
|
|
1002
|
+
when 'start'
|
|
1003
|
+
id_or_index = todo_args.shift
|
|
1004
|
+
unless id_or_index
|
|
1005
|
+
puts "Usage: h #{tm.scope} todo start <id|index>"
|
|
1006
|
+
next
|
|
1007
|
+
end
|
|
1008
|
+
item = todo_manager.start(id_or_index)
|
|
1009
|
+
puts item ? "Started: #{todo_manager.format_item(item)}" : "Item not found: #{id_or_index}"
|
|
1010
|
+
|
|
1011
|
+
when 'done'
|
|
1012
|
+
id_or_index = todo_args.shift
|
|
1013
|
+
unless id_or_index
|
|
1014
|
+
puts "Usage: h #{tm.scope} todo done <id|index>"
|
|
1015
|
+
next
|
|
1016
|
+
end
|
|
1017
|
+
item = todo_manager.done(id_or_index)
|
|
1018
|
+
puts item ? "Done: #{todo_manager.format_item(item)}" : "Item not found: #{id_or_index}"
|
|
1019
|
+
|
|
1020
|
+
when 'skip'
|
|
1021
|
+
id_or_index = todo_args.shift
|
|
1022
|
+
unless id_or_index
|
|
1023
|
+
puts "Usage: h #{tm.scope} todo skip <id|index>"
|
|
1024
|
+
next
|
|
1025
|
+
end
|
|
1026
|
+
item = todo_manager.skip(id_or_index)
|
|
1027
|
+
puts item ? "Skipped: #{todo_manager.format_item(item)}" : "Item not found: #{id_or_index}"
|
|
1028
|
+
|
|
1029
|
+
when 'search'
|
|
1030
|
+
query = todo_args.join(' ')
|
|
1031
|
+
if query.empty?
|
|
1032
|
+
puts "Usage: h #{tm.scope} todo search <query>"
|
|
1033
|
+
next
|
|
1034
|
+
end
|
|
1035
|
+
items = todo_manager.search(query)
|
|
1036
|
+
if items.empty?
|
|
1037
|
+
puts "No items matching: #{query}"
|
|
1038
|
+
else
|
|
1039
|
+
puts todo_manager.list(items)
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
else
|
|
1043
|
+
puts "Usage: h #{tm.scope} todo <ls|add|rm|start|done|skip|search> [args]"
|
|
1044
|
+
end
|
|
1045
|
+
end
|
|
926
1046
|
end
|
|
927
1047
|
|
|
928
1048
|
attach_methods task_hiiro, tm
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hiiro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.69
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Toyota
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: pry
|
|
@@ -85,6 +85,7 @@ files:
|
|
|
85
85
|
- bin/h-serve
|
|
86
86
|
- bin/h-session
|
|
87
87
|
- bin/h-sha
|
|
88
|
+
- bin/h-todo
|
|
88
89
|
- bin/h-video
|
|
89
90
|
- bin/h-window
|
|
90
91
|
- bin/h-wtree
|
|
@@ -111,6 +112,7 @@ files:
|
|
|
111
112
|
- lib/hiiro/options.rb
|
|
112
113
|
- lib/hiiro/prefix_matcher.rb
|
|
113
114
|
- lib/hiiro/sk.rb
|
|
115
|
+
- lib/hiiro/todo.rb
|
|
114
116
|
- lib/hiiro/version.rb
|
|
115
117
|
- notes
|
|
116
118
|
- plugins/notify.rb
|