na 1.2.34 → 1.2.37

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class App
4
+ extend GLI::App
5
+ desc 'Undo the last change'
6
+ long_desc 'Run without argument to undo most recent change'
7
+ arg_name 'FILE', optional: true, multiple: true
8
+ command %i[undo] do |c|
9
+ c.example 'na undo', desc: 'Undo the last change'
10
+ c.example 'na undo myproject', desc: 'Undo the last change to a file matching "myproject"'
11
+
12
+ c.action do |_global_options, options, args|
13
+ if args.empty?
14
+ NA.restore_last_modified_file
15
+ else
16
+ args.each do |arg|
17
+ NA.restore_last_modified_file(search: arg)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -63,7 +63,7 @@ class App
63
63
  c.desc 'Delete an action'
64
64
  c.switch %i[delete], negatable: false
65
65
 
66
- c.desc "Open action in editor (#{NA.default_editor}).
66
+ c.desc "Open action in editor (#{NA::Editor.default_editor}).
67
67
  Natural language dates will be parsed and converted in date-based tags."
68
68
  c.switch %i[edit], negatable: false
69
69
 
@@ -97,6 +97,8 @@ class App
97
97
  options[:tagged] << '+done'
98
98
  elsif !options[:remove].nil? && !options[:remove].empty?
99
99
  options[:tagged].concat(options[:remove])
100
+ elsif options[:finish]
101
+ options[:tagged] << '-done'
100
102
  end
101
103
 
102
104
  action = if args.count.positive?
@@ -211,7 +213,15 @@ class App
211
213
 
212
214
  end
213
215
  else
214
- files = NA.find_files(depth: options[:depth])
216
+ files = NA.find_files_matching({
217
+ depth: options[:depth],
218
+ done: options[:done],
219
+ project: target_proj,
220
+ regex: options[:regex],
221
+ require_na: false,
222
+ search: tokens,
223
+ tag: tags
224
+ })
215
225
  NA.notify('{r}No todo file found', exit_code: 1) if files.count.zero?
216
226
 
217
227
  targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
data/lib/na/action.rb CHANGED
@@ -18,6 +18,32 @@ module NA
18
18
  @note = note
19
19
  end
20
20
 
21
+ def process(priority: 0, finish: false, add_tag: [], remove_tag: [], note: [])
22
+ string = @action.dup
23
+
24
+ if priority&.positive?
25
+ string.gsub!(/(?<=\A| )@priority\(\d+\)/, '').strip!
26
+ string += " @priority(#{priority})"
27
+ end
28
+
29
+ add_tag.each do |tag|
30
+ string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
31
+ string.strip!
32
+ string += " @#{tag}"
33
+ end
34
+
35
+ remove_tag.each do |tag|
36
+ string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
37
+ string.strip!
38
+ end
39
+
40
+ string = "#{string.strip} @done(#{Time.now.strftime('%Y-%m-%d %H:%M')})" if finish && string !~ /(?<=\A| )@done/
41
+
42
+ @action = string
43
+ @action.expand_date_tags
44
+ @note = note unless note.empty?
45
+ end
46
+
21
47
  def to_s
22
48
  note = if @note.count.positive?
23
49
  "\n#{@note.join("\n")}"
@@ -37,7 +63,18 @@ module NA
37
63
  EOINSPECT
38
64
  end
39
65
 
66
+ ##
67
+ ## Pretty print an action
68
+ ##
69
+ ## @param extension [String] The file extension
70
+ ## @param template [Hash] The template to use for
71
+ ## colorization
72
+ ## @param regexes [Array] The regexes to
73
+ ## highlight (searches)
74
+ ## @param notes [Boolean] Include notes
75
+ ##
40
76
  def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false)
77
+ # Default colorization, can be overridden with full or partial template variable
41
78
  default_template = {
42
79
  file: '{xbk}',
43
80
  parent: '{c}',
@@ -51,32 +88,38 @@ module NA
51
88
  note: '{dw}'
52
89
  }
53
90
  template = default_template.merge(template)
54
-
91
+ # Create the hierarchical parent string
55
92
  parents = @parent.map do |par|
56
93
  NA::Color.template("#{template[:parent]}#{par}")
57
94
  end.join(NA::Color.template(template[:parent_divider]))
58
95
  parents = "{dc}[{x}#{parents}{dc}]{x} "
59
96
 
97
+ # Create the project string
60
98
  project = NA::Color.template("#{template[:project]}#{@project}{x} ")
61
99
 
100
+ # Create the source filename string, substituting ~ for HOME and removing extension
62
101
  file = @file.sub(%r{^\./}, '').sub(/#{ENV['HOME']}/, '~')
63
102
  file = file.sub(/\.#{extension}$/, '')
103
+ # colorize the basename
64
104
  file = file.sub(/#{File.basename(@file, ".#{extension}")}$/, "{dw}#{File.basename(@file, ".#{extension}")}{x}")
65
105
  file_tpl = "#{template[:file]}#{file} {x}"
66
106
  filename = NA::Color.template(file_tpl)
67
107
 
108
+ # Add notes if needed
68
109
  note = if notes && @note.count.positive?
69
110
  NA::Color.template("\n#{@note.map { |l| " #{template[:note]}• #{l}{x}" }.join("\n")}")
70
111
  else
71
112
  ''
72
113
  end
73
114
 
115
+ # colorize the action and highlight tags
74
116
  action = NA::Color.template("#{template[:action]}#{@action.sub(/ @#{NA.na_tag}\b/, '')}{x}")
75
117
  action = action.highlight_tags(color: template[:tags],
76
118
  parens: template[:value_parens],
77
119
  value: template[:values],
78
120
  last_color: template[:action])
79
121
 
122
+ # Replace variables in template string and output colorized
80
123
  NA::Color.template(template[:output].gsub(/%filename/, filename)
81
124
  .gsub(/%project/, project)
82
125
  .gsub(/%parents?/, parents)
@@ -162,7 +205,7 @@ module NA
162
205
  date = Time.parse(date.strftime('%Y-%m-%d 12:00'))
163
206
  end
164
207
 
165
- puts "Comparing #{tag_date} #{tag[:comp]} #{date}" if NA.verbose
208
+ # NA.notify("{dw}Comparing #{tag_date} #{tag[:comp]} #{date}{x}", debug: true)
166
209
 
167
210
  case tag[:comp]
168
211
  when /^>$/
data/lib/na/actions.rb ADDED
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NA
4
+ # Actions controller
5
+ class Actions < Array
6
+ def initialize(actions = [])
7
+ super
8
+ concat(actions)
9
+ end
10
+
11
+ ##
12
+ ## Pretty print a list of actions
13
+ ##
14
+ ## @param actions [Array] The actions
15
+ ## @param depth [Number] The depth
16
+ ## @param files [Array] The files actions originally came from
17
+ ## @param regexes [Array] The regexes used to gather actions
18
+ ##
19
+ def output(depth, files: nil, regexes: [], notes: false, nest: false, nest_projects: false)
20
+ return if files.nil?
21
+
22
+ if nest
23
+ template = '%parent%action'
24
+
25
+ parent_files = {}
26
+ out = []
27
+
28
+ if nest_projects
29
+ each do |action|
30
+ if parent_files.key?(action.file)
31
+ parent_files[action.file].push(action)
32
+ else
33
+ parent_files[action.file] = [action]
34
+ end
35
+ end
36
+
37
+ parent_files.each do |file, acts|
38
+ projects = NA.project_hierarchy(acts)
39
+ out.push("#{file.sub(%r{^./}, '').shorten_path}:")
40
+ out.concat(NA.output_children(projects, 0))
41
+ end
42
+ else
43
+ template = '%parent%action'
44
+
45
+ each do |action|
46
+ if parent_files.key?(action.file)
47
+ parent_files[action.file].push(action)
48
+ else
49
+ parent_files[action.file] = [action]
50
+ end
51
+ end
52
+
53
+ parent_files.each do |k, v|
54
+ out.push("#{k.sub(%r{^\./}, '')}:")
55
+ v.each do |a|
56
+ out.push("\t- [#{a.parent.join('/')}] #{a.action}")
57
+ out.push("\t\t#{a.note.join("\n\t\t")}") unless a.note.empty?
58
+ end
59
+ end
60
+ end
61
+ NA::Pager.page out.join("\n")
62
+ else
63
+ template = if files.count.positive?
64
+ if files.count == 1
65
+ '%parent%action'
66
+ else
67
+ '%filename%parent%action'
68
+ end
69
+ elsif NA.find_files(depth: depth).count > 1
70
+ if depth > 1
71
+ '%filename%parent%action'
72
+ else
73
+ '%project%parent%action'
74
+ end
75
+ else
76
+ '%parent%action'
77
+ end
78
+ template += '%note' if notes
79
+
80
+ files.map { |f| NA.notify("{dw}#{f}", debug: true) } if files
81
+
82
+ output = map { |action| action.pretty(template: { output: template }, regexes: regexes, notes: notes) }
83
+ NA::Pager.page(output.join("\n"))
84
+ end
85
+ end
86
+ end
87
+ end
data/lib/na/editor.rb ADDED
@@ -0,0 +1,123 @@
1
+ module NA
2
+ module Editor
3
+ class << self
4
+ def default_editor
5
+ editor ||= ENV['NA_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
6
+
7
+ if editor.good? && TTY::Which.exist?(editor)
8
+ return editor
9
+ end
10
+
11
+ notify('No EDITOR environment variable, testing available editors', debug: true)
12
+ editors = %w[vim vi code subl mate mvim nano emacs]
13
+ editors.each do |ed|
14
+ try = TTY::Which.which(ed)
15
+ if try
16
+ notify("Using editor #{try}", debug: true)
17
+ return try
18
+ end
19
+ end
20
+
21
+ notify('{br}No editor found{x}', exit_code: 5)
22
+
23
+ nil
24
+ end
25
+
26
+ def editor_with_args
27
+ args_for_editor(default_editor)
28
+ end
29
+
30
+ def args_for_editor(editor)
31
+ return editor if editor =~ /-\S/
32
+
33
+ args = case editor
34
+ when /^(subl|code|mate)$/
35
+ ['-w']
36
+ when /^(vim|mvim)$/
37
+ ['-f']
38
+ else
39
+ []
40
+ end
41
+ "#{editor} #{args.join(' ')}"
42
+ end
43
+
44
+ ##
45
+ ## Create a process for an editor and wait for the file handle to return
46
+ ##
47
+ ## @param input [String] Text input for editor
48
+ ##
49
+ def fork_editor(input = '', message: :default)
50
+ # raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
51
+
52
+ notify('{br}No EDITOR variable defined in environment{x}', exit_code: 5) if default_editor.nil?
53
+
54
+ tmpfile = Tempfile.new(['na_temp', '.na'])
55
+
56
+ File.open(tmpfile.path, 'w+') do |f|
57
+ f.puts input
58
+ unless message.nil?
59
+ f.puts message == :default ? '# First line is the action, lines after are added as a note' : message
60
+ end
61
+ end
62
+
63
+ pid = Process.fork { system("#{editor_with_args} #{tmpfile.path}") }
64
+
65
+ trap('INT') do
66
+ begin
67
+ Process.kill(9, pid)
68
+ rescue StandardError
69
+ Errno::ESRCH
70
+ end
71
+ tmpfile.unlink
72
+ tmpfile.close!
73
+ exit 0
74
+ end
75
+
76
+ Process.wait(pid)
77
+
78
+ begin
79
+ if $?.exitstatus == 0
80
+ input = IO.read(tmpfile.path)
81
+ else
82
+ exit_now! 'Cancelled'
83
+ end
84
+ ensure
85
+ tmpfile.close
86
+ tmpfile.unlink
87
+ end
88
+
89
+ input.split(/\n/).delete_if(&:ignore?).join("\n")
90
+ end
91
+
92
+ ##
93
+ ## Takes a multi-line string and formats it as an entry
94
+ ##
95
+ ## @param input [String] The string to parse
96
+ ##
97
+ ## @return [Array] [[String]title, [Note]note]
98
+ ##
99
+ def format_input(input)
100
+ notify('No content in entry', exit_code: 1) if input.nil? || input.strip.empty?
101
+
102
+ input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
103
+ title = input_lines[0]&.strip
104
+ notify('{br}No content in first line{x}', exit_code: 1) if title.nil? || title.strip.empty?
105
+
106
+ title.expand_date_tags
107
+
108
+ note = if input_lines.length > 1
109
+ input_lines[1..-1]
110
+ else
111
+ []
112
+ end
113
+
114
+ unless note.empty?
115
+ note.map!(&:strip)
116
+ note.delete_if { |l| l =~ /^\s*$/ || l =~ /^#/ }
117
+ end
118
+
119
+ [title, note]
120
+ end
121
+ end
122
+ end
123
+ end