Module: NA::Editor

Included in:
NA
Defined in:
lib/na/editor.rb

Overview

Provides editor selection and argument helpers for launching text editors.

Class Method Summary collapse

Class Method Details

.args_for_editor(editor) ⇒ String

Returns the editor command with appropriate arguments for file opening.

Parameters:

  • editor (String)

    Editor command

Returns:

  • (String)

    Editor command with arguments



45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/na/editor.rb', line 45

def args_for_editor(editor)
  return editor if editor =~ /-\S/

  args = case editor
         when /^(subl|code|mate)$/
           ['-w']
         when /^(vim|mvim)$/
           ['-f']
         else
           []
         end
  "#{editor} #{args.join(' ')}"
end

.default_editor(prefer_git_editor: true) ⇒ String?

Returns the default editor command, checking environment variables and available editors.

Parameters:

  • prefer_git_editor (Boolean) (defaults to: true)

    Prefer GIT_EDITOR over EDITOR

Returns:

  • (String, nil)

    Editor command or nil if not found



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/na/editor.rb', line 12

def default_editor(prefer_git_editor: true)
  editor ||= if prefer_git_editor
               ENV['NA_EDITOR'] || ENV['GIT_EDITOR'] || ENV.fetch('EDITOR', nil)
             else
               ENV['NA_EDITOR'] || ENV['EDITOR'] || ENV.fetch('GIT_EDITOR', nil)
             end

  return editor if editor&.good? && TTY::Which.exist?(editor)

  NA.notify('No EDITOR environment variable, testing available editors', debug: true)
  editors = %w[vim vi code subl mate mvim nano emacs]
  editors.each do |ed|
    try = TTY::Which.which(ed)
    if try
      NA.notify("Using editor #{try}", debug: true)
      return try
    end
  end

  NA.notify("#{NA.theme[:error]}No editor found", exit_code: 5)

  nil
end

.editor_with_argsString

Returns the default editor command with its arguments.

Returns:

  • (String)

    Editor command with arguments



38
39
40
# File 'lib/na/editor.rb', line 38

def editor_with_args
  args_for_editor(default_editor)
end

.fork_editor(input = '', message: :default) ⇒ String

Create a process for an editor and wait for the file handle to return

Parameters:

  • input (String) (defaults to: '')

    Text input for editor

Returns:



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/na/editor.rb', line 63

def fork_editor(input = '', message: :default)
  # raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']

  NA.notify("#{NA.theme[:error]}No EDITOR variable defined in environment", exit_code: 5) if default_editor.nil?

  tmpfile = Tempfile.new(['na_temp', '.na'])

  File.open(tmpfile.path, 'w+') do |f|
    f.puts input
    unless message.nil?
      f.puts message == :default ? '# First line is the action, lines after are added as a note' : message
    end
  end

  pid = Process.fork { system("#{editor_with_args} #{tmpfile.path}") }

  trap('INT') do
    begin
      Process.kill(9, pid)
    rescue StandardError
      Errno::ESRCH
    end
    tmpfile.unlink
    tmpfile.close!
    exit 0
  end

  Process.wait(pid)

  begin
    if $CHILD_STATUS.exitstatus.zero?
      input = File.read(tmpfile.path)
    else
      exit_now! 'Cancelled'
    end
  ensure
    tmpfile.close
    tmpfile.unlink
  end

  # Don't strip comments if this looks like multi-action format (has # ------ markers)
  if input.include?('# ------ ')
    input
  else
    input.split("\n").delete_if(&:ignore?).join("\n")
  end
end

.format_input(input) ⇒ Array

Takes a multi-line string and formats it as an entry

Parameters:

  • input (String)

    The string to parse

Returns:

  • (Array)
    [String]title, [Note]note


115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/na/editor.rb', line 115

def format_input(input)
  NA.notify("#{NA.theme[:error]}No content in entry", exit_code: 1) if input.nil? || input.strip.empty?

  input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
  title = input_lines[0]&.strip
  NA.notify("#{NA.theme[:error]}No content in first line", exit_code: 1) if title.nil? || title.strip.empty?

  title = title.expand_date_tags

  note = if input_lines.length > 1
           input_lines[1..]
         else
           []
         end

  unless note.empty?
    note.map!(&:strip)
    note.delete_if { |l| l =~ /^\s*$/ || l =~ /^#/ }
  end

  [title, note]
end

.format_multi_action_input(actions) ⇒ String

Format multiple actions for multi-edit

Parameters:

Returns:

  • (String)

    Formatted editor content



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/na/editor.rb', line 141

def format_multi_action_input(actions)
  header = <<~EOF
    # Instructions:
    # - Edit the action text (the lines WITHOUT # comment markers)
    # - DO NOT remove or edit the lines starting with "# ------"
    # - Add notes on new lines after the action
    # - Blank lines are ignored
    #

  EOF

  # Use + to create a mutable string
  content = +header

  actions.each do |action|
    # Use file_path to get the path and file_line to get the line number
    content << "# ------ #{action.file_path}:#{action.file_line}\n"
    content << "#{action.action}\n"
    content << "#{action.note.join("\n")}\n" if action.note.any?
    content << "\n" # Blank line separator
  end

  content
end

.parse_multi_action_output(content) ⇒ Hash

Parse multi-action editor output

Parameters:

  • content (String)

    Editor output

Returns:

  • (Hash)

    Hash mapping file:line to [action, note]



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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
# File 'lib/na/editor.rb', line 169

def parse_multi_action_output(content)
  results = {}
  current_file = nil
  current_action = nil
  current_note = []

  content.lines.each do |line|
    stripped = line.strip

    # Check for file marker: # ------ path:line
    match = stripped.match(/^# ------ (.+?):(\d+)$/)
    if match
      # Save previous action if exists
      results[current_file] = [current_action, current_note] if current_file && current_action

      # Start new action
      current_file = "#{match[1]}:#{match[2]}"
      current_action = nil
      current_note = []
      next
    end

    # Skip other comment lines
    next if stripped.start_with?('#')

    # Skip blank lines
    next if stripped.empty?

    # Store as action or note based on what we've seen so far
    if current_action.nil?
      current_action = stripped
    else
      # Subsequent lines are notes
      current_note << stripped
    end
  end

  # Save last action
  results[current_file] = [current_action, current_note] if current_file && current_action

  results
end