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.
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
- (t['started'] || t['start']) && t['done']
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
@@ -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
- res = NA.yn(
359
- NA::Color.template("#{NA.theme[:warning]}Project #{NA.theme[:file]}#{move}#{NA.theme[:warning]} doesn't exist, add it"), default: true
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.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
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