Class: NA::Action

Inherits:
Hash
  • Object
show all
Defined in:
lib/na/action.rb

Overview

Represents a single actionable item in a todo file, with tags, notes, and project context.

Examples:

Create a new action

action = NA::Action.new('todo.txt', 'Inbox', [], '- Buy milk', 1)

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Hash

#deep_freeze, #deep_freeze!, #deep_merge, #deep_thaw, #deep_thaw!, #symbolize_keys

Constructor Details

#initialize(file, project, parent, action, idx, note = []) ⇒ Action

Returns a new instance of Action.

Examples:

action = NA::Action.new('todo.txt', 'Inbox', [], '- Buy milk', 1)


14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/na/action.rb', line 14

def initialize(file, project, parent, action, idx, note = [])
  super()

  # Store file in PATH:LINE format if line is available
  @file = if idx.is_a?(Integer)
            "#{file}:#{idx}"
          else
            file
          end
  @project = project
  @parent = parent
  @action = action.gsub('{', '\\{')
  @tags = scan_tags
  @line = idx
  @note = note
end

Instance Attribute Details

#actionObject

Returns the value of attribute action.



10
11
12
# File 'lib/na/action.rb', line 10

def action
  @action
end

#fileObject (readonly)

Returns the value of attribute file.



9
10
11
# File 'lib/na/action.rb', line 9

def file
  @file
end

#lineObject (readonly)

Returns the value of attribute line.



9
10
11
# File 'lib/na/action.rb', line 9

def line
  @line
end

#noteObject

Returns the value of attribute note.



10
11
12
# File 'lib/na/action.rb', line 10

def note
  @note
end

#parentObject

Returns the value of attribute parent.



10
11
12
# File 'lib/na/action.rb', line 10

def parent
  @parent
end

#projectObject (readonly)

Returns the value of attribute project.



9
10
11
# File 'lib/na/action.rb', line 9

def project
  @project
end

#tagsObject (readonly)

Returns the value of attribute tags.



9
10
11
# File 'lib/na/action.rb', line 9

def tags
  @tags
end

Instance Method Details

#file_lineInteger

Get the line number

Returns:

  • (Integer)

    Line number



72
73
74
# File 'lib/na/action.rb', line 72

def file_line
  file_line_parts.last
end

#file_line_partsArray

Extract file path and line number from PATH:LINE format

Returns:

  • (Array)
    file_path, line_number


53
54
55
56
57
58
59
60
# File 'lib/na/action.rb', line 53

def file_line_parts
  if @file.to_s.include?(':')
    path, line = @file.split(':', 2)
    [path, line.to_i]
  else
    [@file, @line]
  end
end

#file_pathString

Get just the file path without line number

Returns:



65
66
67
# File 'lib/na/action.rb', line 65

def file_path
  file_line_parts.first
end

#inspectString

Inspect the action object

Returns:



164
165
166
167
168
169
170
171
172
173
# File 'lib/na/action.rb', line 164

def inspect
  "    @file: \#{@file}\n    @project: \#{@project}\n    @parent: \#{@parent.join('>')}\n    @action: \#{@action}\n    @tags: \#{@tags}\n    @note: \#{@note}\n  EOINSPECT\nend\n"

#na?Boolean

Returns true if this action contains the current next-action tag (e.g. @na)

Returns:

  • (Boolean)


33
34
35
# File 'lib/na/action.rb', line 33

def na?
  @tags.key?(NA.na_tag)
end

#pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false, detect_width: true) ⇒ String

Pretty print an action with color and template formatting

Parameters:

  • extension (String) (defaults to: 'taskpaper')

    File extension

  • template (Hash) (defaults to: {})

    Color template

  • regexes (Array) (defaults to: [])

    Regexes to highlight

  • notes (Boolean) (defaults to: false)

    Include notes

  • detect_width (Boolean) (defaults to: true)

    Detect terminal width

Returns:



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/na/action.rb', line 184

def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false, detect_width: true)
  NA::Benchmark.measure('Action.pretty') do
    # Use cached theme instead of loading every time
    theme = NA.theme
    # Merge templates if provided
    if template[:templates]
      theme = theme.dup
      theme[:templates] = theme[:templates].merge(template[:templates])
      template = theme.merge(template.reject { |k| k == :templates })
    else
      template = theme.merge(template)
    end

    # Pre-compute common template parts to avoid repeated processing
    output_template = template[:templates][:output]
    needs_filename = output_template.include?('%filename')
    needs_line = output_template.include?('%line')
    needs_parents = output_template.include?('%parents') || output_template.include?('%parent')
    needs_project = output_template.include?('%project')

    # Create the hierarchical parent string (optimized)
    parents = if needs_parents && @parent.any?
                parent_parts = @parent.map { |par| "#{template[:parent]}#{par}" }.join(template[:parent_divider])
                # parent_parts already has color codes embedded, create final string directly
                "#{NA::Color.template("{x}#{template[:bracket]}[")}#{parent_parts}#{NA::Color.template("{x}#{template[:bracket]}]{x}")}"
              else
                ''
              end

    # Create the project string (optimized) - Ensure color reset before project
    project = if needs_project && !@project.empty?
                NA::Color.template("{x}#{template[:project]}#{@project}{x}")
              else
                ''
              end

    # Create the source filename string (optimized)
    filename = if needs_filename
                 path_only = file_path # Extract just the path from PATH:LINE
                 path = path_only.sub(%r{^\./}, '').sub(/#{Dir.home}/, '~')
                 if File.dirname(path) == '.'
                   fname = NA.include_ext ? File.basename(path) : File.basename(path, ".#{extension}")
                   fname = "./#{fname}" if NA.show_cwd_indicator
                   NA::Color.template("#{template[:filename]}#{fname}{x}")
                 else
                   colored = (NA.include_ext ? path : path.sub(/\.#{extension}$/, '')).highlight_filename
                   NA::Color.template("#{template[:filename]}#{colored}{x}")
                 end
               else
                 ''
               end

    # Create the line number string (optimized)
    line_num = if needs_line && @line
                 NA::Color.template("#{template[:line]}:#{@line} {x}")
               else
                 ''
               end

    # colorize the action and highlight tags (optimized)
    action_text = @action.dup
    action_text.gsub!(/\{(.*?)\}/, '\\{\1\\}')
    action_text = action_text.sub(/ @#{NA.na_tag}\b/, '')
    # Reset colors before action to prevent bleeding from parents/project
    action = NA::Color.template("{x}#{template[:action]}#{action_text}{x}")
    action = action.highlight_tags(color: template[:tags],
                                   parens: template[:value_parens],
                                   value: template[:values],
                                   last_color: template[:action])

    # Handle notes and wrapping (optimized)
    note = ''
    if @note.any?
      if notes
        if detect_width
          # Cache width calculation
          width = @cached_width ||= TTY::Screen.columns
          # Calculate indent more efficiently - avoid repeated template processing
          base_template = output_template.gsub('%action', '').gsub('%note', '')
          base_output = base_template.gsub('%filename', filename).gsub('%line', line_num).gsub('%project', project).gsub(/%parents?/,
                                                                                                                         parents)
          indent = NA::Color.uncolor(NA::Color.template(base_output)).length
          note = NA::Color.template(@note.wrap(width, indent, template[:note]))
        else
          note = NA::Color.template("\n#{@note.map { |l| "  {x}#{template[:note]}• #{l}{x}" }.join("\n")}")
        end
      else
        action += "{x}#{template[:note]}*"
      end
    end

    # Wrap action if needed (optimized)
    if detect_width && !action.empty?
      width = @cached_width ||= TTY::Screen.columns
      base_template = output_template.gsub('%action', '').gsub('%note', '')
      base_output = base_template.gsub('%filename', filename).gsub('%line', line_num).gsub('%project', project).gsub(/%parents?/, parents)
      indent = NA::Color.uncolor(NA::Color.template(base_output)).length
      action = action.wrap(width, indent)
    end

    # Replace variables in template string and output colorized (optimized)
    final_output = output_template.dup
    final_output.gsub!('%filename', filename)
    final_output.gsub!('%line', line_num)
    final_output.gsub!('%project', project)
    final_output.gsub!(/%parents?/, parents)
    final_output.gsub!('%action', action.highlight_search(regexes))
    final_output.gsub!('%note', note)
    final_output.gsub!('\\{', '{')

    NA::Color.template(final_output)
  end
end

#process(priority: 0, finish: false, add_tag: [], remove_tag: [], note: [], started_at: nil, done_at: nil, duration_seconds: nil) ⇒ void

This method returns an undefined value.

Update the action string and note with priority, tags, and completion status

Examples:

action.process(priority: 5, finish: true, add_tag: ["urgent"], remove_tag: ["waiting"], note: ["Call Bob"], started_at: Time.now, done_at: Time.now)

Parameters:

  • priority (Integer) (defaults to: 0)

    Priority value to set

  • finish (Boolean) (defaults to: false)

    Mark as finished

  • add_tag (Array<String>) (defaults to: [])

    Tags to add

  • remove_tag (Array<String>) (defaults to: [])

    Tags to remove

  • note (Array<String>) (defaults to: [])

    Notes to set



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/na/action.rb', line 86

def process(priority: 0, finish: false, add_tag: [], remove_tag: [], note: [], started_at: nil, done_at: nil, duration_seconds: nil)
  string = @action.dup

  if priority&.positive?
    string.gsub!(/(?<=\A| )@priority\(\d+\)/, '')
    string.strip!
    string += " @priority(#{priority})"
  end

  remove_tag.each do |tag|
    string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
    string.strip!
  end

  add_tag.each do |tag|
    string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
    string.strip!
    string += " @#{tag}"
  end

  # Compute started/done from duration if provided
  if duration_seconds && (done_at || finish)
    done_time = done_at || Time.now
    started_at ||= done_time - duration_seconds.to_i
  elsif duration_seconds && started_at
    done_at ||= started_at + duration_seconds.to_i
  end

  # Insert @started if provided
  if started_at
    string.gsub!(/(?<=\A| )@start(?:ed)?\(.*?\)/i, '')
    string.strip!
    string += " @started(#{started_at.strftime('%Y-%m-%d %H:%M')})"
  end

  # Insert @done if provided or finishing
  if done_at
    string.gsub!(/(?<=\A| )@done\(.*?\)/i, '')
    string.strip!
    string += " @done(#{done_at.strftime('%Y-%m-%d %H:%M')})"
  elsif finish && string !~ /(?<=\A| )@done/
    string = "#{string.strip} @done(#{Time.now.strftime('%Y-%m-%d %H:%M')})"
  end

  @action = string.expand_date_tags
  @tags = scan_tags
  @note = note unless note.empty?
end

#scan_tagsObject



321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/na/action.rb', line 321

def scan_tags
  tags = {}
  rx = /(?<= |^)@(?<tag>\S+?)(?:\((?<value>.*?)\))?(?= |$)/
  all_tags = []
  @action.scan(rx) { all_tags << Regexp.last_match }
  all_tags.each do |m|
    tag = m.named_captures.symbolize_keys
    tags[tag[:tag]] = tag[:value]
  end

  tags
end

#search_match?(any: [], all: [], none: [], include_note: true) ⇒ Boolean

Check if action or note matches any, all, and none search criteria

Parameters:

  • any (Array) (defaults to: [])

    Regexes to match any

  • all (Array) (defaults to: [])

    Regexes to match all

  • none (Array) (defaults to: [])

    Regexes to match none

  • include_note (Boolean) (defaults to: true)

    Include note in search

Returns:

  • (Boolean)


315
316
317
318
319
# File 'lib/na/action.rb', line 315

def search_match?(any: [], all: [], none: [], include_note: true)
  search_matches_any?(any, include_note: include_note) &&
    search_matches_all?(all, include_note: include_note) &&
    search_matches_none?(none, include_note: include_note)
end

#tags_match?(any: [], all: [], none: []) ⇒ Boolean

Check if action tags match any, all, and none criteria

Parameters:

  • any (Array) (defaults to: [])

    Tags to match any

  • all (Array) (defaults to: [])

    Tags to match all

  • none (Array) (defaults to: [])

    Tags to match none

Returns:

  • (Boolean)


304
305
306
# File 'lib/na/action.rb', line 304

def tags_match?(any: [], all: [], none: [])
  tag_matches_any?(any) && tag_matches_all?(all) && tag_matches_none?(none)
end

#to_plugin_io_hashHash

Convert action to plugin IO hash

Returns:



39
40
41
42
43
44
45
46
47
48
# File 'lib/na/action.rb', line 39

def to_plugin_io_hash
  {
    'file_path' => file_path,
    'line' => file_line,
    'parents' => [@project].concat(@parent),
    'text' => @action.dup,
    'note' => @note.join("\n"),
    'tags' => @tags.map { |k, v| { 'name' => k, 'value' => (v || '').to_s } }
  }
end

#to_sString

String representation of the action

Examples:

action.to_s #=> "{ project: 'Inbox', ... }"

Returns:



140
141
142
143
144
145
146
147
# File 'lib/na/action.rb', line 140

def to_s
  note = if @note.any?
           "\n#{@note.join("\n")}"
         else
           ''
         end
  "(#{file_path}:#{file_line}) #{@project}:#{@parent.join(">")} | #{@action}#{note}"
end

#to_s_prettyString

Pretty string representation of the action with color formatting

Returns:



152
153
154
155
156
157
158
159
# File 'lib/na/action.rb', line 152

def to_s_pretty
  note = if @note.any?
           "\n#{@note.join("\n")}"
         else
           ''
         end
  "{x}#{NA.theme[:filename]}#{File.basename(file_path)}:#{file_line}{x}#{NA.theme[:bracket]}[{x}#{NA.theme[:project]}#{@project}:#{@parent.join('>')}{x}#{NA.theme[:bracket]}]{x} | #{NA.theme[:action]}#{@action}{x}#{NA.theme[:note]}#{note}"
end