na 1.2.80 → 1.2.81
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/.rubocop.yml +8 -2
- data/.rubocop_todo.yml +33 -538
- data/CHANGELOG.md +19 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +27 -10
- data/README.md +46 -5
- data/Rakefile +6 -0
- data/bin/commands/next.rb +4 -0
- data/bin/commands/scan.rb +84 -0
- data/bin/commands/update.rb +1 -1
- data/bin/na +7 -7
- data/lib/na/action.rb +101 -35
- data/lib/na/actions.rb +79 -77
- data/lib/na/array.rb +11 -7
- data/lib/na/benchmark.rb +21 -9
- data/lib/na/colors.rb +84 -86
- data/lib/na/editor.rb +22 -22
- data/lib/na/hash.rb +32 -9
- data/lib/na/help_monkey_patch.rb +9 -1
- data/lib/na/next_action.rb +314 -245
- data/lib/na/pager.rb +38 -14
- data/lib/na/project.rb +14 -1
- data/lib/na/prompt.rb +25 -3
- data/lib/na/string.rb +90 -130
- data/lib/na/theme.rb +37 -31
- data/lib/na/todo.rb +149 -131
- data/lib/na/version.rb +3 -1
- data/lib/na.rb +1 -0
- data/na.gemspec +4 -2
- data/scripts/generate-fish-completions.rb +18 -21
- data/src/_README.md +14 -4
- data/test_performance.rb +5 -5
- metadata +53 -24
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
na (1.2.
|
|
4
|
+
na (1.2.81)
|
|
5
5
|
chronic (~> 0.10, >= 0.10.2)
|
|
6
6
|
git (~> 3.0.0)
|
|
7
7
|
gli (~> 2.21.0)
|
|
@@ -14,14 +14,14 @@ PATH
|
|
|
14
14
|
GEM
|
|
15
15
|
remote: https://rubygems.org/
|
|
16
16
|
specs:
|
|
17
|
-
activesupport (8.0
|
|
17
|
+
activesupport (8.1.0)
|
|
18
18
|
base64
|
|
19
|
-
benchmark (>= 0.3)
|
|
20
19
|
bigdecimal
|
|
21
20
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
22
21
|
connection_pool (>= 2.2.5)
|
|
23
22
|
drb
|
|
24
23
|
i18n (>= 1.6, < 2)
|
|
24
|
+
json
|
|
25
25
|
logger (>= 1.4.2)
|
|
26
26
|
minitest (>= 5.1)
|
|
27
27
|
securerandom (>= 0.3)
|
|
@@ -30,11 +30,12 @@ GEM
|
|
|
30
30
|
addressable (2.8.7)
|
|
31
31
|
public_suffix (>= 2.0.2, < 7.0)
|
|
32
32
|
base64 (0.3.0)
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
bigdecimal (3.3.1)
|
|
34
|
+
bump (0.6.1)
|
|
35
35
|
chronic (0.10.2)
|
|
36
36
|
concurrent-ruby (1.3.5)
|
|
37
|
-
connection_pool (2.5.
|
|
37
|
+
connection_pool (2.5.4)
|
|
38
|
+
diff-lcs (1.6.2)
|
|
38
39
|
drb (2.2.3)
|
|
39
40
|
git (3.0.2)
|
|
40
41
|
activesupport (>= 5.0)
|
|
@@ -44,15 +45,29 @@ GEM
|
|
|
44
45
|
gli (2.21.5)
|
|
45
46
|
i18n (1.14.7)
|
|
46
47
|
concurrent-ruby (~> 1.0)
|
|
48
|
+
json (2.15.2)
|
|
47
49
|
logger (1.7.0)
|
|
48
50
|
mdless (1.0.37)
|
|
49
|
-
minitest (5.
|
|
50
|
-
ostruct (0.6.
|
|
51
|
+
minitest (5.26.0)
|
|
52
|
+
ostruct (0.6.3)
|
|
51
53
|
process_executer (1.3.0)
|
|
52
54
|
public_suffix (6.0.2)
|
|
53
55
|
rake (13.3.0)
|
|
54
|
-
rchardet (1.
|
|
56
|
+
rchardet (1.10.0)
|
|
55
57
|
rdoc (4.3.0)
|
|
58
|
+
rspec (3.13.0)
|
|
59
|
+
rspec-core (~> 3.13.0)
|
|
60
|
+
rspec-expectations (~> 3.13.0)
|
|
61
|
+
rspec-mocks (~> 3.13.0)
|
|
62
|
+
rspec-core (3.13.3)
|
|
63
|
+
rspec-support (~> 3.13.0)
|
|
64
|
+
rspec-expectations (3.13.5)
|
|
65
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
66
|
+
rspec-support (~> 3.13.0)
|
|
67
|
+
rspec-mocks (3.13.4)
|
|
68
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
69
|
+
rspec-support (~> 3.13.0)
|
|
70
|
+
rspec-support (3.13.6)
|
|
56
71
|
securerandom (0.4.1)
|
|
57
72
|
tty-cursor (0.7.1)
|
|
58
73
|
tty-reader (0.9.0)
|
|
@@ -65,7 +80,7 @@ GEM
|
|
|
65
80
|
tty-which (0.5.0)
|
|
66
81
|
tzinfo (2.0.6)
|
|
67
82
|
concurrent-ruby (~> 1.0)
|
|
68
|
-
uri (1.0.
|
|
83
|
+
uri (1.0.4)
|
|
69
84
|
wisper (2.0.1)
|
|
70
85
|
|
|
71
86
|
PLATFORMS
|
|
@@ -82,10 +97,12 @@ PLATFORMS
|
|
|
82
97
|
x86_64-linux-musl
|
|
83
98
|
|
|
84
99
|
DEPENDENCIES
|
|
100
|
+
bump (~> 0.6.0)
|
|
85
101
|
minitest (~> 5.14)
|
|
86
102
|
na!
|
|
87
103
|
rake
|
|
88
104
|
rdoc (~> 4.3)
|
|
105
|
+
rspec (~> 3.0)
|
|
89
106
|
tty-spinner (~> 0.9, >= 0.9.0)
|
|
90
107
|
|
|
91
108
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
_If you're one of the rare people like me who find this useful, feel free to
|
|
10
10
|
[buy me some coffee][donate]._
|
|
11
11
|
|
|
12
|
-
The current version of `na` is 1.2.
|
|
12
|
+
The current version of `na` is 1.2.81.
|
|
13
13
|
|
|
14
|
-
`na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder.
|
|
14
|
+
`na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder.
|
|
15
15
|
|
|
16
16
|
Used with Taskpaper files, it can add new action items quickly from the command line, automatically tagging them as next actions. It can also mark actions as completed, delete them, archive them, and move them between projects.
|
|
17
17
|
|
|
@@ -48,7 +48,7 @@ You can list next actions in files in the current directory by typing `na`. By d
|
|
|
48
48
|
|
|
49
49
|
#### Adding todos
|
|
50
50
|
|
|
51
|
-
You can also quickly add todo items from the command line with the `add` subcommand. The script will look for a file in the current directory with a `.taskpaper` extension (configurable).
|
|
51
|
+
You can also quickly add todo items from the command line with the `add` subcommand. The script will look for a file in the current directory with a `.taskpaper` extension (configurable).
|
|
52
52
|
|
|
53
53
|
If found, it will try to locate an `Inbox:` project, or create one if it doesn't exist. Any arguments after `add` will be combined to create a new task in TaskPaper format. They will automatically be assigned as next actions (tagged `@na`) and will show up when `na` lists the tasks for the project.
|
|
54
54
|
|
|
@@ -76,7 +76,7 @@ SYNOPSIS
|
|
|
76
76
|
na [global options] command [command options] [arguments...]
|
|
77
77
|
|
|
78
78
|
VERSION
|
|
79
|
-
1.2.
|
|
79
|
+
1.2.81
|
|
80
80
|
|
|
81
81
|
GLOBAL OPTIONS
|
|
82
82
|
-a, --add - Add a next action (deprecated, for backwards compatibility)
|
|
@@ -116,6 +116,7 @@ COMMANDS
|
|
|
116
116
|
prompt - Show or install prompt hooks for the current shell
|
|
117
117
|
restore, unfinish - Find and remove @done tag from an action
|
|
118
118
|
saved - Execute a saved search
|
|
119
|
+
scan - Scan a directory tree for todo files and cache them
|
|
119
120
|
tag - Add tags to matching action(s)
|
|
120
121
|
tagged - Find actions matching a tag
|
|
121
122
|
todos - Show list of known todo files
|
|
@@ -133,7 +134,7 @@ If you run the `add` command with no arguments, you'll be asked for input on the
|
|
|
133
134
|
|
|
134
135
|
###### Adding notes
|
|
135
136
|
|
|
136
|
-
Use the `--note` switch to add a note. If STDIN (piped) input is present when this switch is used, it will be included in the note. A prompt will be displayed for adding additional notes, which will be appended to any STDIN note passed. Press CTRL-d to end editing and save the note.
|
|
137
|
+
Use the `--note` switch to add a note. If STDIN (piped) input is present when this switch is used, it will be included in the note. A prompt will be displayed for adding additional notes, which will be appended to any STDIN note passed. Press CTRL-d to end editing and save the note.
|
|
137
138
|
|
|
138
139
|
Notes are not displayed by the `next/tagged/find` commands unless `--notes` is specified.
|
|
139
140
|
|
|
@@ -335,6 +336,7 @@ COMMAND OPTIONS
|
|
|
335
336
|
--[no-]done - Include @done actions
|
|
336
337
|
--exact - Search query is exact text match (not tokens)
|
|
337
338
|
--file=TODO_FILE - Display matches from specific todo file ([relative] path) (default: none)
|
|
339
|
+
--hidden - Include hidden directories while traversing
|
|
338
340
|
--in, --todo=TODO - Display matches from a known todo file anywhere in history (short name) (may be used more than once, default: none)
|
|
339
341
|
--nest - Output actions nested by file
|
|
340
342
|
--no_file - No filename in output
|
|
@@ -426,6 +428,44 @@ EXAMPLES
|
|
|
426
428
|
na saved
|
|
427
429
|
```
|
|
428
430
|
|
|
431
|
+
##### scan
|
|
432
|
+
|
|
433
|
+
Scan a directory tree for todo files and cache them in tdlist.txt. Avoids duplicates and can optionally prune non-existent entries.
|
|
434
|
+
|
|
435
|
+
Scan reports how many files were added and, if --prune is used, how many were pruned. With --dry-run, it lists the full file paths that would be added and/or pruned.
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
NAME
|
|
439
|
+
scan - Scan a directory tree for todo files and cache them
|
|
440
|
+
|
|
441
|
+
SYNOPSIS
|
|
442
|
+
|
|
443
|
+
na [global options] scan [command options] [PATH]
|
|
444
|
+
|
|
445
|
+
DESCRIPTION
|
|
446
|
+
Searches PATH (default: current directory) for files matching the current NA.extension and adds their absolute paths to the tdlist.txt cache. Avoids duplicates. Optionally prunes non-existent entries from the cache.
|
|
447
|
+
|
|
448
|
+
COMMAND OPTIONS
|
|
449
|
+
-d, --depth=DEPTH - Recurse to depth (1..N or i/inf for infinite) (default: 5)
|
|
450
|
+
--hidden - Include hidden directories and files while scanning
|
|
451
|
+
-n, --dry-run - Show what would be added/pruned, but do not write tdlist.txt
|
|
452
|
+
-p, --prune - Prune removed files from cache after scan
|
|
453
|
+
|
|
454
|
+
EXAMPLES
|
|
455
|
+
|
|
456
|
+
# Scan current directory up to default depth (5)
|
|
457
|
+
na scan
|
|
458
|
+
|
|
459
|
+
# Scan a specific path up to depth 3
|
|
460
|
+
na scan -d 3 ~/Projects
|
|
461
|
+
|
|
462
|
+
# Scan current directory recursively with no depth limit
|
|
463
|
+
na scan -d inf
|
|
464
|
+
|
|
465
|
+
# Prune non-existent entries from the cache (in addition to scanning)
|
|
466
|
+
na scan --prune
|
|
467
|
+
```
|
|
468
|
+
|
|
429
469
|
##### tagged
|
|
430
470
|
|
|
431
471
|
Example: `na tagged feature +maybe`.
|
|
@@ -453,6 +493,7 @@ COMMAND OPTIONS
|
|
|
453
493
|
--[no-]done - Include @done actions
|
|
454
494
|
--exact - Search query is exact text match (not tokens)
|
|
455
495
|
--file=TODO_FILE - Display matches from specific todo file ([relative] path) (default: none)
|
|
496
|
+
--hidden - Include hidden directories while traversing
|
|
456
497
|
--in, --todo=TODO - Display matches from a known todo file anywhere in history (short name) (may be used more than once, default: none)
|
|
457
498
|
--nest - Output actions nested by file
|
|
458
499
|
--no_file - No filename in output
|
data/Rakefile
CHANGED
data/bin/commands/next.rb
CHANGED
|
@@ -19,6 +19,9 @@ class App
|
|
|
19
19
|
c.arg_name "DEPTH"
|
|
20
20
|
c.flag %i[d depth], type: :integer, must_match: /^[1-9]$/
|
|
21
21
|
|
|
22
|
+
c.desc "Include hidden directories while traversing"
|
|
23
|
+
c.switch %i[hidden], negatable: false, default_value: false
|
|
24
|
+
|
|
22
25
|
c.desc "Show next actions from all known todo files (in any directory)"
|
|
23
26
|
c.switch %i[all], negatable: false, default_value: false
|
|
24
27
|
|
|
@@ -217,6 +220,7 @@ class App
|
|
|
217
220
|
file_path = options[:file] ? File.expand_path(options[:file]) : nil
|
|
218
221
|
|
|
219
222
|
todo = NA::Todo.new({ depth: depth,
|
|
223
|
+
hidden: options[:hidden],
|
|
220
224
|
done: options[:done],
|
|
221
225
|
file_path: file_path,
|
|
222
226
|
project: options[:project],
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class App
|
|
4
|
+
extend GLI::App
|
|
5
|
+
desc 'Scan a directory tree for todo files and cache them'
|
|
6
|
+
long_desc 'Searches PATH (default: current directory) for files matching the current NA.extension
|
|
7
|
+
and adds their absolute paths to the tdlist.txt cache. Avoids duplicates. Optionally prunes
|
|
8
|
+
non-existent entries from the cache.'
|
|
9
|
+
arg_name 'PATH', optional: true
|
|
10
|
+
command %i[scan] do |c|
|
|
11
|
+
c.example 'na scan', desc: 'Scan current directory up to default depth (5)'
|
|
12
|
+
c.example 'na scan -d 3 ~/Projects', desc: 'Scan a specific path up to depth 3'
|
|
13
|
+
c.example 'na scan -d inf', desc: 'Scan current directory recursively with no depth limit'
|
|
14
|
+
c.example 'na scan --prune', desc: 'Prune non-existent entries from the cache (in addition to scanning)'
|
|
15
|
+
|
|
16
|
+
c.desc 'Recurse to depth (1..N or i/inf for infinite)'
|
|
17
|
+
c.arg_name 'DEPTH'
|
|
18
|
+
c.default_value '5'
|
|
19
|
+
c.flag %i[d depth], must_match: /^(\d+|i\w*)$/i
|
|
20
|
+
|
|
21
|
+
c.desc 'Prune removed files from cache after scan'
|
|
22
|
+
c.switch %i[p prune], negatable: false, default_value: false
|
|
23
|
+
|
|
24
|
+
c.desc 'Include hidden directories and files while scanning'
|
|
25
|
+
c.switch %i[hidden], negatable: false, default_value: false
|
|
26
|
+
|
|
27
|
+
c.desc 'Show what would be added/pruned, but do not write tdlist.txt'
|
|
28
|
+
c.switch %i[n dry-run], negatable: false, default_value: false
|
|
29
|
+
|
|
30
|
+
c.action do |_global_options, options, args|
|
|
31
|
+
base = args.first || Dir.pwd
|
|
32
|
+
ext = NA.extension
|
|
33
|
+
|
|
34
|
+
# Parse depth: numeric or starts-with-i for infinite
|
|
35
|
+
depth_arg = (options[:depth] || '5').to_s
|
|
36
|
+
infinite = depth_arg =~ /^i/i ? true : false
|
|
37
|
+
depth = infinite ? nil : depth_arg.to_i
|
|
38
|
+
depth = 5 if depth.nil? && !infinite
|
|
39
|
+
|
|
40
|
+
# Prepare existing cache
|
|
41
|
+
db = NA.database_path
|
|
42
|
+
existing = if File.exist?(db)
|
|
43
|
+
File.read(db).split(/\n/).map(&:strip)
|
|
44
|
+
else
|
|
45
|
+
[]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
found = []
|
|
49
|
+
Dir.chdir(base) do
|
|
50
|
+
patterns = if infinite
|
|
51
|
+
["*.#{ext}", "**/*.#{ext}"]
|
|
52
|
+
else
|
|
53
|
+
(1..[depth, 1].max).map { |d| (d > 1 ? ('*/' * (d - 1)) : '') + "*.#{ext}" }
|
|
54
|
+
end
|
|
55
|
+
pattern = patterns.length == 1 ? patterns.first : "{#{patterns.join(',')}}"
|
|
56
|
+
files = Dir.glob(pattern, File::FNM_DOTMATCH)
|
|
57
|
+
# Exclude hidden dirs/files (any segment starting with '.') unless --hidden
|
|
58
|
+
files.reject! { |f| f.split('/').any? { |seg| seg.start_with?('.') && seg !~ /^\.\.?$/ } } unless options[:hidden]
|
|
59
|
+
found = files.map { |f| File.expand_path(f) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
merged = (existing + found).map(&:strip).uniq.sort
|
|
63
|
+
merged.select! { |f| File.exist?(f) } if options[:prune]
|
|
64
|
+
|
|
65
|
+
added_files = (merged - existing)
|
|
66
|
+
pruned_files = options[:prune] ? (existing - merged) : []
|
|
67
|
+
added = added_files.count
|
|
68
|
+
pruned = pruned_files.count
|
|
69
|
+
|
|
70
|
+
if options[:dry_run]
|
|
71
|
+
msg = "#{NA.theme[:success]}Dry run: would add #{added} file#{added == 1 ? '' : 's'}"
|
|
72
|
+
msg << ", prune #{pruned} file#{pruned == 1 ? '' : 's'}" if options[:prune]
|
|
73
|
+
NA.notify(msg)
|
|
74
|
+
NA.notify("{bw}Would add:{x}\n#{added_files.join("\n")}") if added_files.any?
|
|
75
|
+
NA.notify("{bw}Would prune:{x}\n#{pruned_files.join("\n")}") if options[:prune] && pruned_files.any?
|
|
76
|
+
else
|
|
77
|
+
File.open(db, 'w') { |f| f.puts merged.join("\n") }
|
|
78
|
+
msg = "#{NA.theme[:success]}Scan complete: #{NA.theme[:filename]}#{added}{x}#{NA.theme[:success]} added"
|
|
79
|
+
msg << ", #{NA.theme[:filename]}#{pruned}{x}#{NA.theme[:success]} pruned" if options[:prune]
|
|
80
|
+
NA.notify(msg)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/bin/commands/update.rb
CHANGED
|
@@ -192,7 +192,7 @@ class App
|
|
|
192
192
|
# Require at least one actionable option to be provided
|
|
193
193
|
actionable = [
|
|
194
194
|
options[:note],
|
|
195
|
-
(options[:priority].to_i if options[:priority]).to_i
|
|
195
|
+
(options[:priority].to_i if options[:priority]).to_i.positive?,
|
|
196
196
|
!options[:move].to_s.empty?,
|
|
197
197
|
!(options[:tag].nil? || options[:tag].empty?),
|
|
198
198
|
!(options[:remove].nil? || options[:remove].empty?),
|
data/bin/na
CHANGED
|
@@ -10,11 +10,11 @@ require 'fcntl'
|
|
|
10
10
|
require 'tempfile'
|
|
11
11
|
|
|
12
12
|
NA::Benchmark.init
|
|
13
|
-
NA::Benchmark.measure('Gem loading') { nil }
|
|
13
|
+
NA::Benchmark.measure('Gem loading') { nil } # Measures time up to this point
|
|
14
14
|
|
|
15
15
|
# Search for XDG compliant config first. Default to ~/.na.rc for compatibility
|
|
16
16
|
def self.find_config_file
|
|
17
|
-
home =
|
|
17
|
+
home = Dir.home
|
|
18
18
|
xdg_config_home = ENV['XDG_CONFIG_HOME'] || File.join(home, '.config')
|
|
19
19
|
|
|
20
20
|
rc_paths = [
|
|
@@ -30,7 +30,6 @@ def self.find_config_file
|
|
|
30
30
|
existing_path || File.join(xdg_config_home, 'na', 'na.rc')
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
|
|
34
33
|
# Main application
|
|
35
34
|
class App
|
|
36
35
|
extend GLI::App
|
|
@@ -84,7 +83,7 @@ class App
|
|
|
84
83
|
switch %i[repo-top], default_value: false
|
|
85
84
|
|
|
86
85
|
desc 'Provide a template for new/blank todo files, use initconfig to make permanent'
|
|
87
|
-
flag %
|
|
86
|
+
flag %(template)
|
|
88
87
|
|
|
89
88
|
desc 'Use current working directory as [p]roject, [t]ag, or [n]one'
|
|
90
89
|
arg_name 'TYPE'
|
|
@@ -121,7 +120,7 @@ class App
|
|
|
121
120
|
NA.include_ext = global[:include_ext]
|
|
122
121
|
NA.na_tag = global[:na_tag]
|
|
123
122
|
NA.global_file = global[:file]
|
|
124
|
-
NA.cwd = File.basename(ENV
|
|
123
|
+
NA.cwd = File.basename(ENV.fetch('PWD', nil))
|
|
125
124
|
NA.cwd_is = if global[:cwd_as] =~ /^n/
|
|
126
125
|
:none
|
|
127
126
|
else
|
|
@@ -146,7 +145,8 @@ class App
|
|
|
146
145
|
NA.global_file = taskpaper_file
|
|
147
146
|
# Add this block to create the file if it doesn't exist
|
|
148
147
|
unless File.exist?(taskpaper_file)
|
|
149
|
-
res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Repository file not found, create #{taskpaper_file}"),
|
|
148
|
+
res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Repository file not found, create #{taskpaper_file}"),
|
|
149
|
+
default: true)
|
|
150
150
|
if res
|
|
151
151
|
NA.create_todo(taskpaper_file, repo_name, template: global[:template])
|
|
152
152
|
else
|
|
@@ -198,7 +198,7 @@ class App
|
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
NA.stdin = $stdin.read.strip if $stdin.stat.size.positive? || $stdin.fcntl(Fcntl::F_GETFL, 0).zero?
|
|
201
|
-
NA.stdin = nil unless NA.stdin
|
|
201
|
+
NA.stdin = nil unless NA.stdin&.length&.positive?
|
|
202
202
|
|
|
203
203
|
NA.globals = []
|
|
204
204
|
NA.command_line = []
|
data/lib/na/action.rb
CHANGED
|
@@ -12,12 +12,20 @@ module NA
|
|
|
12
12
|
@file = file
|
|
13
13
|
@project = project
|
|
14
14
|
@parent = parent
|
|
15
|
-
@action = action.gsub(
|
|
15
|
+
@action = action.gsub('{', '\\{')
|
|
16
16
|
@tags = scan_tags
|
|
17
17
|
@line = idx
|
|
18
18
|
@note = note
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Update the action string and note with priority, tags, and completion status
|
|
22
|
+
#
|
|
23
|
+
# @param priority [Integer] Priority value to set
|
|
24
|
+
# @param finish [Boolean] Mark as finished
|
|
25
|
+
# @param add_tag [Array<String>] Tags to add
|
|
26
|
+
# @param remove_tag [Array<String>] Tags to remove
|
|
27
|
+
# @param note [Array<String>] Notes to set
|
|
28
|
+
# @return [void]
|
|
21
29
|
def process(priority: 0, finish: false, add_tag: [], remove_tag: [], note: [])
|
|
22
30
|
string = @action.dup
|
|
23
31
|
|
|
@@ -44,6 +52,9 @@ module NA
|
|
|
44
52
|
@note = note unless note.empty?
|
|
45
53
|
end
|
|
46
54
|
|
|
55
|
+
# String representation of the action
|
|
56
|
+
#
|
|
57
|
+
# @return [String]
|
|
47
58
|
def to_s
|
|
48
59
|
note = if @note.count.positive?
|
|
49
60
|
"\n#{@note.join("\n")}"
|
|
@@ -53,36 +64,41 @@ module NA
|
|
|
53
64
|
"(#{@file}:#{@line}) #{@project}:#{@parent.join('>')} | #{@action}#{note}"
|
|
54
65
|
end
|
|
55
66
|
|
|
67
|
+
# Pretty string representation of the action with color formatting
|
|
68
|
+
#
|
|
69
|
+
# @return [String]
|
|
56
70
|
def to_s_pretty
|
|
57
71
|
note = if @note.count.positive?
|
|
58
72
|
"\n#{@note.join("\n")}"
|
|
59
73
|
else
|
|
60
74
|
''
|
|
61
75
|
end
|
|
62
|
-
"#{NA.theme[:filename]}#{File.basename(@file)}:#{@line}#{NA.theme[:bracket]}[#{NA.theme[:project]}#{@project}:#{@parent.join(
|
|
76
|
+
"{x}#{NA.theme[:filename]}#{File.basename(@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}"
|
|
63
77
|
end
|
|
64
78
|
|
|
79
|
+
# Inspect the action object
|
|
80
|
+
#
|
|
81
|
+
# @return [String]
|
|
65
82
|
def inspect
|
|
66
83
|
<<~EOINSPECT
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
@file: #{@file}
|
|
85
|
+
@project: #{@project}
|
|
86
|
+
@parent: #{@parent.join('>')}
|
|
87
|
+
@action: #{@action}
|
|
88
|
+
@tags: #{@tags}
|
|
89
|
+
@note: #{@note}
|
|
73
90
|
EOINSPECT
|
|
74
91
|
end
|
|
75
92
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
##
|
|
93
|
+
#
|
|
94
|
+
# Pretty print an action with color and template formatting
|
|
95
|
+
#
|
|
96
|
+
# @param extension [String] File extension
|
|
97
|
+
# @param template [Hash] Color template
|
|
98
|
+
# @param regexes [Array] Regexes to highlight
|
|
99
|
+
# @param notes [Boolean] Include notes
|
|
100
|
+
# @param detect_width [Boolean] Detect terminal width
|
|
101
|
+
# @return [String]
|
|
86
102
|
def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false, detect_width: true)
|
|
87
103
|
NA::Benchmark.measure('Action.pretty') do
|
|
88
104
|
# Use cached theme instead of loading every time
|
|
@@ -98,24 +114,29 @@ module NA
|
|
|
98
114
|
# Create the hierarchical parent string (optimized)
|
|
99
115
|
parents = if needs_parents && @parent.any?
|
|
100
116
|
parent_parts = @parent.map { |par| "#{template[:parent]}#{par}" }.join(template[:parent_divider])
|
|
101
|
-
NA::Color.template("{x}#{template[:bracket]}[#{template[:error]}#{parent_parts}#{template[:bracket]}]{x} ")
|
|
117
|
+
NA::Color.template("{x}#{template[:bracket]}[#{template[:error]}#{parent_parts}{x}#{template[:bracket]}]{x} ")
|
|
102
118
|
else
|
|
103
119
|
''
|
|
104
120
|
end
|
|
105
121
|
|
|
106
122
|
# Create the project string (optimized)
|
|
107
123
|
project = if needs_project && !@project.empty?
|
|
108
|
-
NA::Color.template("#{template[:project]}#{@project}{x} ")
|
|
124
|
+
NA::Color.template("{x}#{template[:project]}#{@project}{x} ")
|
|
109
125
|
else
|
|
110
126
|
''
|
|
111
127
|
end
|
|
112
128
|
|
|
113
129
|
# Create the source filename string (optimized)
|
|
114
130
|
filename = if needs_filename
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
path = @file.sub(%r{^\./}, '').sub(/#{Dir.home}/, '~')
|
|
132
|
+
if File.dirname(path) == '.'
|
|
133
|
+
fname = NA.include_ext ? File.basename(path) : File.basename(path, ".#{extension}")
|
|
134
|
+
fname = "./#{fname}" if NA.show_cwd_indicator
|
|
135
|
+
NA::Color.template("#{template[:filename]}#{fname} {x}")
|
|
136
|
+
else
|
|
137
|
+
colored = (NA.include_ext ? path : path.sub(/\.#{extension}$/, '')).highlight_filename
|
|
138
|
+
NA::Color.template("#{template[:filename]}#{colored} {x}")
|
|
139
|
+
end
|
|
119
140
|
else
|
|
120
141
|
''
|
|
121
142
|
end
|
|
@@ -138,44 +159,58 @@ module NA
|
|
|
138
159
|
# Cache width calculation
|
|
139
160
|
width = @cached_width ||= TTY::Screen.columns
|
|
140
161
|
# Calculate indent more efficiently - avoid repeated template processing
|
|
141
|
-
base_template = output_template.gsub(
|
|
142
|
-
base_output = base_template.gsub(
|
|
162
|
+
base_template = output_template.gsub('%action', '').gsub('%note', '')
|
|
163
|
+
base_output = base_template.gsub('%filename', filename).gsub('%project', project).gsub(/%parents?/,
|
|
164
|
+
parents)
|
|
143
165
|
indent = NA::Color.uncolor(NA::Color.template(base_output)).length
|
|
144
166
|
note = NA::Color.template(@note.wrap(width, indent, template[:note]))
|
|
145
167
|
else
|
|
146
|
-
note = NA::Color.template("\n#{@note.map { |l| " #{template[:note]}• #{l}{x}" }.join("\n")}")
|
|
168
|
+
note = NA::Color.template("\n#{@note.map { |l| " {x}#{template[:note]}• #{l}{x}" }.join("\n")}")
|
|
147
169
|
end
|
|
148
170
|
else
|
|
149
|
-
action += "#{template[:note]}*"
|
|
171
|
+
action += "{x}#{template[:note]}*"
|
|
150
172
|
end
|
|
151
173
|
end
|
|
152
174
|
|
|
153
175
|
# Wrap action if needed (optimized)
|
|
154
176
|
if detect_width && !action.empty?
|
|
155
177
|
width = @cached_width ||= TTY::Screen.columns
|
|
156
|
-
base_template = output_template.gsub(
|
|
157
|
-
base_output = base_template.gsub(
|
|
178
|
+
base_template = output_template.gsub('%action', '').gsub('%note', '')
|
|
179
|
+
base_output = base_template.gsub('%filename', filename).gsub('%project', project).gsub(/%parents?/, parents)
|
|
158
180
|
indent = NA::Color.uncolor(NA::Color.template(base_output)).length
|
|
159
181
|
action = action.wrap(width, indent)
|
|
160
182
|
end
|
|
161
183
|
|
|
162
184
|
# Replace variables in template string and output colorized (optimized)
|
|
163
185
|
final_output = output_template.dup
|
|
164
|
-
final_output.gsub!(
|
|
165
|
-
final_output.gsub!(
|
|
186
|
+
final_output.gsub!('%filename', filename)
|
|
187
|
+
final_output.gsub!('%project', project)
|
|
166
188
|
final_output.gsub!(/%parents?/, parents)
|
|
167
|
-
final_output.gsub!(
|
|
168
|
-
final_output.gsub!(
|
|
169
|
-
final_output.gsub!(
|
|
189
|
+
final_output.gsub!('%action', action.highlight_search(regexes))
|
|
190
|
+
final_output.gsub!('%note', note)
|
|
191
|
+
final_output.gsub!('\\{', '{')
|
|
170
192
|
|
|
171
193
|
NA::Color.template(final_output)
|
|
172
194
|
end
|
|
173
195
|
end
|
|
174
196
|
|
|
197
|
+
# Check if action tags match any, all, and none criteria
|
|
198
|
+
#
|
|
199
|
+
# @param any [Array] Tags to match any
|
|
200
|
+
# @param all [Array] Tags to match all
|
|
201
|
+
# @param none [Array] Tags to match none
|
|
202
|
+
# @return [Boolean]
|
|
175
203
|
def tags_match?(any: [], all: [], none: [])
|
|
176
204
|
tag_matches_any(any) && tag_matches_all(all) && tag_matches_none(none)
|
|
177
205
|
end
|
|
178
206
|
|
|
207
|
+
# Check if action or note matches any, all, and none search criteria
|
|
208
|
+
#
|
|
209
|
+
# @param any [Array] Regexes to match any
|
|
210
|
+
# @param all [Array] Regexes to match all
|
|
211
|
+
# @param none [Array] Regexes to match none
|
|
212
|
+
# @param include_note [Boolean] Include note in search
|
|
213
|
+
# @return [Boolean]
|
|
179
214
|
def search_match?(any: [], all: [], none: [], include_note: true)
|
|
180
215
|
search_matches_any(any, include_note: include_note) &&
|
|
181
216
|
search_matches_all(all, include_note: include_note) &&
|
|
@@ -184,6 +219,11 @@ module NA
|
|
|
184
219
|
|
|
185
220
|
private
|
|
186
221
|
|
|
222
|
+
# Check if action and note do not match any regexes
|
|
223
|
+
#
|
|
224
|
+
# @param regexes [Array] Regexes to check
|
|
225
|
+
# @param include_note [Boolean] Include note in search
|
|
226
|
+
# @return [Boolean]
|
|
187
227
|
def search_matches_none(regexes, include_note: true)
|
|
188
228
|
regexes.each do |rx|
|
|
189
229
|
regex = rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE)
|
|
@@ -193,6 +233,11 @@ module NA
|
|
|
193
233
|
true
|
|
194
234
|
end
|
|
195
235
|
|
|
236
|
+
# Check if action or note matches any regexes
|
|
237
|
+
#
|
|
238
|
+
# @param regexes [Array] Regexes to check
|
|
239
|
+
# @param include_note [Boolean] Include note in search
|
|
240
|
+
# @return [Boolean]
|
|
196
241
|
def search_matches_any(regexes, include_note: true)
|
|
197
242
|
return true if regexes.empty?
|
|
198
243
|
|
|
@@ -204,6 +249,11 @@ module NA
|
|
|
204
249
|
false
|
|
205
250
|
end
|
|
206
251
|
|
|
252
|
+
# Check if action or note matches all regexes
|
|
253
|
+
#
|
|
254
|
+
# @param regexes [Array] Regexes to check
|
|
255
|
+
# @param include_note [Boolean] Include note in search
|
|
256
|
+
# @return [Boolean]
|
|
207
257
|
def search_matches_all(regexes, include_note: true)
|
|
208
258
|
regexes.each do |rx|
|
|
209
259
|
regex = rx.is_a?(Regexp) ? rx : Regexp.new(rx, Regexp::IGNORECASE)
|
|
@@ -213,6 +263,10 @@ module NA
|
|
|
213
263
|
true
|
|
214
264
|
end
|
|
215
265
|
|
|
266
|
+
# Check if none of the tags match
|
|
267
|
+
#
|
|
268
|
+
# @param tags [Array] Tags to check
|
|
269
|
+
# @return [Boolean]
|
|
216
270
|
def tag_matches_none(tags)
|
|
217
271
|
tags.each do |tag|
|
|
218
272
|
return false if compare_tag(tag)
|
|
@@ -220,6 +274,10 @@ module NA
|
|
|
220
274
|
true
|
|
221
275
|
end
|
|
222
276
|
|
|
277
|
+
# Check if any of the tags match
|
|
278
|
+
#
|
|
279
|
+
# @param tags [Array] Tags to check
|
|
280
|
+
# @return [Boolean]
|
|
223
281
|
def tag_matches_any(tags)
|
|
224
282
|
return true if tags.empty?
|
|
225
283
|
|
|
@@ -229,6 +287,10 @@ module NA
|
|
|
229
287
|
false
|
|
230
288
|
end
|
|
231
289
|
|
|
290
|
+
# Check if all of the tags match
|
|
291
|
+
#
|
|
292
|
+
# @param tags [Array] Tags to check
|
|
293
|
+
# @return [Boolean]
|
|
232
294
|
def tag_matches_all(tags)
|
|
233
295
|
tags.each do |tag|
|
|
234
296
|
return false unless compare_tag(tag)
|
|
@@ -236,6 +298,10 @@ module NA
|
|
|
236
298
|
true
|
|
237
299
|
end
|
|
238
300
|
|
|
301
|
+
# Compare a tag against the action's tags with optional value comparison
|
|
302
|
+
#
|
|
303
|
+
# @param tag [Hash] Tag criteria
|
|
304
|
+
# @return [Boolean]
|
|
239
305
|
def compare_tag(tag)
|
|
240
306
|
tag_regex = tag[:tag].is_a?(Regexp) ? tag[:tag] : Regexp.new(tag[:tag], Regexp::IGNORECASE)
|
|
241
307
|
keys = @tags.keys.delete_if { |k| k !~ tag_regex }
|