na 1.2.87 → 1.2.89
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/.cursor/commands/changelog.md +3 -1
- data/.rubocop_todo.yml +24 -16
- data/CHANGELOG.md +75 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +4 -1
- data/README.md +322 -45
- data/bin/commands/find.rb +62 -0
- data/bin/commands/next.rb +65 -0
- data/bin/commands/plugin.rb +289 -0
- data/bin/commands/tagged.rb +63 -0
- data/bin/commands/update.rb +169 -85
- data/bin/na +1 -0
- data/lib/na/action.rb +19 -0
- data/lib/na/actions.rb +3 -2
- data/lib/na/next_action.rb +105 -7
- data/lib/na/plugins.rb +564 -0
- data/lib/na/string.rb +6 -4
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +1 -0
- data/na/Test.todo.markdown +32 -0
- data/na/test.md +21 -0
- data/na.gemspec +1 -0
- data/plugins.md +38 -0
- data/src/_README.md +240 -44
- metadata +20 -1
data/lib/na/action.rb
CHANGED
|
@@ -28,6 +28,25 @@ module NA
|
|
|
28
28
|
@note = note
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Returns true if this action contains the current next-action tag (e.g. @na)
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def na?
|
|
34
|
+
@tags.key?(NA.na_tag)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Convert action to plugin IO hash
|
|
38
|
+
# @return [Hash]
|
|
39
|
+
def to_plugin_io_hash
|
|
40
|
+
{
|
|
41
|
+
'file_path' => file_path,
|
|
42
|
+
'line' => file_line,
|
|
43
|
+
'parents' => [@project].concat(@parent),
|
|
44
|
+
'text' => @action.dup,
|
|
45
|
+
'note' => @note.join("\n"),
|
|
46
|
+
'tags' => @tags.map { |k, v| { 'name' => k, 'value' => (v || '').to_s } }
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
31
50
|
# Extract file path and line number from PATH:LINE format
|
|
32
51
|
#
|
|
33
52
|
# @return [Array] [file_path, line_number]
|
data/lib/na/actions.rb
CHANGED
|
@@ -40,7 +40,8 @@ module NA
|
|
|
40
40
|
filtered_actions = if config[:only_timed]
|
|
41
41
|
self.select do |a|
|
|
42
42
|
t = a.tags
|
|
43
|
-
|
|
43
|
+
tl = t.transform_keys { |k| k.to_s.downcase }
|
|
44
|
+
(tl['started'] || tl['start']) && tl['done']
|
|
44
45
|
end
|
|
45
46
|
else
|
|
46
47
|
self
|
|
@@ -118,7 +119,7 @@ module NA
|
|
|
118
119
|
|
|
119
120
|
if config[:times]
|
|
120
121
|
# compute duration from @started/@done
|
|
121
|
-
tags = action.tags
|
|
122
|
+
tags = action.tags.transform_keys { |k| k.to_s.downcase }
|
|
122
123
|
begun = tags['started'] || tags['start']
|
|
123
124
|
finished = tags['done']
|
|
124
125
|
if begun && finished
|
data/lib/na/next_action.rb
CHANGED
|
@@ -3,6 +3,97 @@
|
|
|
3
3
|
# Next Action methods
|
|
4
4
|
module NA
|
|
5
5
|
class << self
|
|
6
|
+
# Select actions across files using existing search pipeline
|
|
7
|
+
# @return [Array<NA::Action>]
|
|
8
|
+
def select_actions(file: nil, depth: 1, search: [], tagged: [], include_done: false)
|
|
9
|
+
files = if file
|
|
10
|
+
[file]
|
|
11
|
+
else
|
|
12
|
+
find_files(depth: depth)
|
|
13
|
+
end
|
|
14
|
+
out = []
|
|
15
|
+
files.each do |f|
|
|
16
|
+
_projects, actions = find_actions(f, search, tagged, done: include_done, all: true)
|
|
17
|
+
out.concat(actions) if actions
|
|
18
|
+
end
|
|
19
|
+
out
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Apply a plugin result hash back to the underlying file
|
|
23
|
+
# - Move if parents changed (project path differs)
|
|
24
|
+
# - Update text/note/tags
|
|
25
|
+
def apply_plugin_result(io_hash)
|
|
26
|
+
file = io_hash['file_path']
|
|
27
|
+
line = io_hash['line'].to_i
|
|
28
|
+
parents = Array(io_hash['parents']).map(&:to_s)
|
|
29
|
+
text = io_hash['text'].to_s
|
|
30
|
+
note = io_hash['note'].to_s
|
|
31
|
+
tags = Array(io_hash['tags']).to_h { |t| [t['name'].to_s, t['value'].to_s] }
|
|
32
|
+
action_block = io_hash['action'] || { 'action' => 'UPDATE', 'arguments' => [] }
|
|
33
|
+
action_name = action_block['action'].to_s.upcase
|
|
34
|
+
action_args = Array(action_block['arguments'])
|
|
35
|
+
|
|
36
|
+
# Load current action
|
|
37
|
+
_projects, actions = find_actions(file, nil, nil, all: true, done: true, project: nil, search_note: true, target_line: line)
|
|
38
|
+
action = actions&.first
|
|
39
|
+
return unless action
|
|
40
|
+
|
|
41
|
+
# Determine new project path from parents array
|
|
42
|
+
new_project = ''
|
|
43
|
+
new_parent_chain = []
|
|
44
|
+
if parents.any?
|
|
45
|
+
new_project = parents.first.to_s
|
|
46
|
+
new_parent_chain = parents[1..] || []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
case action_name
|
|
50
|
+
when 'DELETE'
|
|
51
|
+
update_action(file, { target_line: line }, delete: true, all: true)
|
|
52
|
+
return
|
|
53
|
+
when 'COMPLETE'
|
|
54
|
+
update_action(file, { target_line: line }, finish: true, all: true)
|
|
55
|
+
return
|
|
56
|
+
when 'RESTORE'
|
|
57
|
+
update_action(file, { target_line: line }, restore: true, all: true)
|
|
58
|
+
return
|
|
59
|
+
when 'ARCHIVE'
|
|
60
|
+
update_action(file, { target_line: line }, finish: true, move: 'Archive', all: true)
|
|
61
|
+
return
|
|
62
|
+
when 'ADD_TAG'
|
|
63
|
+
add_tags = action_args.map { |t| t.sub(/^@/, '') }
|
|
64
|
+
update_action(file, { target_line: line }, add: action, add_tag: add_tags, all: true)
|
|
65
|
+
return
|
|
66
|
+
when 'DELETE_TAG', 'REMOVE_TAG'
|
|
67
|
+
remove_tags = action_args.map { |t| t.sub(/^@/, '') }
|
|
68
|
+
update_action(file, { target_line: line }, add: action, remove_tag: remove_tags, all: true)
|
|
69
|
+
return
|
|
70
|
+
when 'MOVE'
|
|
71
|
+
move_to = action_args.first.to_s
|
|
72
|
+
update_action(file, { target_line: line }, add: action, move: move_to, all: true, suppress_prompt: true)
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Replace content on the existing action then write back in-place
|
|
77
|
+
original_line = action.file_line
|
|
78
|
+
original_project = action.project
|
|
79
|
+
original_parent_chain = action.parent.dup
|
|
80
|
+
|
|
81
|
+
# Update action content
|
|
82
|
+
action.action = text
|
|
83
|
+
action.note = note.to_s.split("\n")
|
|
84
|
+
action.action.gsub!(/(?<=\A| )@\S+(?:\(.*?\))?/, '')
|
|
85
|
+
unless tags.empty?
|
|
86
|
+
tag_str = tags.map { |k, v| v.to_s.empty? ? "@#{k}" : "@#{k}(#{v})" }.join(' ')
|
|
87
|
+
action.action = action.action.strip + (tag_str.empty? ? "" : " #{tag_str}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if parents changed
|
|
91
|
+
parents_changed = new_project.to_s.strip != original_project || new_parent_chain != original_parent_chain
|
|
92
|
+
move_to = parents_changed ? ([new_project] + new_parent_chain).join(':') : nil
|
|
93
|
+
|
|
94
|
+
# Update in-place (with move if parents changed)
|
|
95
|
+
update_action(file, { target_line: original_line }, add: action, move: move_to, all: true, suppress_prompt: true)
|
|
96
|
+
end
|
|
6
97
|
include NA::Editor
|
|
7
98
|
|
|
8
99
|
attr_accessor :verbose, :extension, :include_ext, :na_tag, :command_line, :command, :globals, :global_file,
|
|
@@ -51,6 +142,7 @@ module NA
|
|
|
51
142
|
# @return [Boolean] result
|
|
52
143
|
#
|
|
53
144
|
def yn(prompt, default: true)
|
|
145
|
+
return default if ENV['NA_TEST'] == '1'
|
|
54
146
|
return default unless $stdout.isatty
|
|
55
147
|
|
|
56
148
|
tty_state = `stty -g`
|
|
@@ -330,7 +422,8 @@ module NA
|
|
|
330
422
|
tagged: nil,
|
|
331
423
|
started_at: nil,
|
|
332
424
|
done_at: nil,
|
|
333
|
-
duration_seconds: nil
|
|
425
|
+
duration_seconds: nil,
|
|
426
|
+
suppress_prompt: false)
|
|
334
427
|
# Coerce date/time inputs if passed as strings
|
|
335
428
|
begin
|
|
336
429
|
started_at = NA::Types.parse_date_begin(started_at) if started_at && !started_at.is_a?(Time)
|
|
@@ -355,14 +448,19 @@ module NA
|
|
|
355
448
|
move = move.sub(/:$/, '')
|
|
356
449
|
target_proj = projects.select { |pr| pr.project =~ /#{move.gsub(':', '.*?:.*?')}/i }.first
|
|
357
450
|
if target_proj.nil?
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
)
|
|
361
|
-
if res
|
|
362
|
-
target_proj = insert_project(target, move, projects)
|
|
451
|
+
if suppress_prompt || !$stdout.isatty
|
|
452
|
+
target_proj = insert_project(target, move)
|
|
363
453
|
projects << target_proj
|
|
364
454
|
else
|
|
365
|
-
NA.
|
|
455
|
+
res = NA.yn(
|
|
456
|
+
NA::Color.template("#{NA.theme[:warning]}Project #{NA.theme[:file]}#{move}#{NA.theme[:warning]} doesn't exist, add it"), default: true
|
|
457
|
+
)
|
|
458
|
+
if res
|
|
459
|
+
target_proj = insert_project(target, move)
|
|
460
|
+
projects << target_proj
|
|
461
|
+
else
|
|
462
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
|
|
463
|
+
end
|
|
366
464
|
end
|
|
367
465
|
end
|
|
368
466
|
end
|