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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3064cf620431cbe446ea9083ab57784fd509c37257f803624d9ed70d9448f10
4
- data.tar.gz: 0b487eed0954f696eb5289aa2f6b205c87b96d87d112c93d7e1e6b2ef587c12e
3
+ metadata.gz: 70e0ff0419923a10f71567fafbb03fed3c13a31bd3b338acb018cfc9f3dce8ec
4
+ data.tar.gz: 4d20bf045c56dde0ec03fdd9b7d47b24cb73aba90cf1871c36ffd206fce46b61
5
5
  SHA512:
6
- metadata.gz: 60c6e5dd58849a83302753253b9e075a501b86b1e2a56bf609122a5c159cfdbd70161df02170f761120a2c9c4c58f4067ff4f4aa2bfd959f4ef9e2265e4f6fde
7
- data.tar.gz: 12e76894beac7a152c78afac0598b62230c15a8d90cfad04667b79e75f9dcd2cdab34130ffdcf75b63021fca783be080e43b30b42f997c8ff579f55b6e316df8
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
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.68"
2
+ VERSION = "0.1.69"
3
3
  end
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.68
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-10 00:00:00.000000000 Z
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