tty-editor 0.4.0 → 0.7.0

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/tty-editor.rb CHANGED
@@ -1,3 +1 @@
1
- # encoding: utf-8
2
-
3
- require_relative 'tty/editor'
1
+ require_relative "tty/editor"
data/lib/tty/editor.rb CHANGED
@@ -1,141 +1,209 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- require 'tty-prompt'
4
- require 'tty-which'
5
- require 'tempfile'
6
- require 'fileutils'
7
- require 'shellwords'
3
+ require "pathname"
4
+ require "shellwords"
5
+ require "tempfile"
6
+ require "tty-prompt"
8
7
 
9
- require_relative 'editor/version'
8
+ require_relative "editor/version"
10
9
 
11
10
  module TTY
12
11
  # A class responsible for launching an editor
13
12
  #
14
13
  # @api public
15
14
  class Editor
15
+ Error = Class.new(StandardError)
16
+
17
+ # Raised when user provides unnexpected or incorrect argument
18
+ InvalidArgumentError = Class.new(Error)
19
+
16
20
  # Raised when command cannot be invoked
17
21
  class CommandInvocationError < RuntimeError; end
18
22
 
19
23
  # Raised when editor cannot be found
20
24
  class EditorNotFoundError < RuntimeError; end
21
25
 
22
- # Check if editor exists
26
+ # List possible command line text editors
23
27
  #
24
- # @return [Boolean]
28
+ # @return [Array<String>]
25
29
  #
26
- # @api private
27
- def self.exist?(cmd)
28
- TTY::Which.exist?(cmd)
29
- end
30
+ # @api public
31
+ EXECUTABLES = [
32
+ "atom", "code", "emacs", "gedit", "jed", "kate",
33
+ "mate -w", "mg", "nano -w", "notepad", "pico",
34
+ "qe", "subl -n -w", "vi", "vim"
35
+ ].freeze
30
36
 
31
- # Check if Windowz
37
+ # Check if editor command exists
38
+ #
39
+ # @example
40
+ # exist?("vim") # => true
41
+ #
42
+ # @example
43
+ # exist?("/usr/local/bin/vim") # => true
44
+ #
45
+ # @example
46
+ # exist?("C:\\Program Files\\Vim\\vim.exe") # => true
47
+ #
48
+ # @param [String] command
49
+ # the command to check for the existence
32
50
  #
33
51
  # @return [Boolean]
34
52
  #
35
- # @api public
36
- def self.windows?
37
- ::File::ALT_SEPARATOR == "\\"
53
+ # @api private
54
+ def self.exist?(command)
55
+ path = Pathname(command)
56
+ exts = [""].concat(ENV.fetch("PATHEXT", "").split(::File::PATH_SEPARATOR))
57
+
58
+ if path.absolute?
59
+ return exts.any? { |ext| ::File.exist?("#{command}#{ext}") }
60
+ end
61
+
62
+ ENV.fetch("PATH", "").split(::File::PATH_SEPARATOR).any? do |dir|
63
+ file = ::File.join(dir, command)
64
+ exts.any? { |ext| ::File.exist?("#{file}#{ext}") }
65
+ end
38
66
  end
39
67
 
40
68
  # Check editor from environment variables
41
69
  #
42
- # @return [Array[String]]
70
+ # @return [Array<String>]
43
71
  #
44
72
  # @api public
45
73
  def self.from_env
46
- [ENV['VISUAL'], ENV['EDITOR']].compact
74
+ [ENV["VISUAL"], ENV["EDITOR"]].compact
47
75
  end
48
76
 
49
- # List possible executable for editor command
50
- #
51
- # @return [Array[String]]
77
+ # Find available text editors
52
78
  #
53
- # @api public
54
- def self.executables
55
- ['vim', 'vi', 'emacs', 'nano', 'nano-tiny', 'pico', 'mate -w']
56
- end
57
-
58
- # Find available command
59
- #
60
- # @param [Array[String]] commands
79
+ # @param [Array<String>] commands
61
80
  # the commands to use intstead of defaults
62
81
  #
63
- # @return [Array[String]]
82
+ # @return [Array<String>]
83
+ # the existing editor commands
64
84
  #
65
85
  # @api public
66
86
  def self.available(*commands)
67
- return commands unless commands.empty?
87
+ if commands.any?
88
+ execs = search_executables(commands.map(&:to_s))
89
+ return execs unless execs.empty?
90
+ end
68
91
 
69
- if !from_env.all?(&:empty?)
70
- [from_env.find { |e| !e.empty? }]
71
- elsif windows?
72
- ['notepad']
73
- else
74
- executables.uniq.select(&method(:exist?))
92
+ if from_env.any?
93
+ execs = search_executables(from_env)
94
+ return execs unless execs.empty?
75
95
  end
96
+
97
+ search_executables(EXECUTABLES)
76
98
  end
77
99
 
100
+ # Search for existing executables
101
+ #
102
+ # @return [Array<String>]
103
+ #
104
+ # @api private
105
+ def self.search_executables(execs)
106
+ execs.compact.map(&:strip).reject(&:empty?).uniq
107
+ .select { |exec| exist?(exec.split.first) }
108
+ end
109
+ private_class_method :search_executables
110
+
78
111
  # Open file in system editor
79
112
  #
80
113
  # @example
81
- # TTY::Editor.open('filename.rb')
114
+ # TTY::Editor.open("/path/to/filename")
82
115
  #
83
- # @param [String] file
84
- # the name of the file
116
+ # @example
117
+ # TTY::Editor.open("file1", "file2", "file3")
118
+ #
119
+ # @example
120
+ # TTY::Editor.open(text: "Some text")
121
+ #
122
+ # @param [Array<String>] files
123
+ # the files to open in an editor
124
+ # @param [String] command
125
+ # the editor command to use, by default auto detects
126
+ # @param [String] text
127
+ # the text to edit in an editor
128
+ # @param [Hash] env
129
+ # environment variables to forward to the editor
85
130
  #
86
131
  # @return [Object]
87
132
  #
88
133
  # @api public
89
- def self.open(*args)
90
- editor = new(*args)
91
-
92
- yield(editor) if block_given?
93
-
94
- editor.open
134
+ def self.open(*files, text: nil, **options, &block)
135
+ editor = new(**options, &block)
136
+ editor.open(*files, text: text)
95
137
  end
96
138
 
97
139
  # Initialize an Editor
98
140
  #
99
- # @param [String] file
100
- # @param [Hash[Symbol]] options
101
- # @option options [Hash] :command
141
+ # @example
142
+ # TTY::Editor.new(command: "vim")
143
+ #
144
+ # @param [String] command
102
145
  # the editor command to use, by default auto detects
103
- # @option options [Hash] :env
146
+ # @param [Hash] env
104
147
  # environment variables to forward to the editor
148
+ # @param [IO] input
149
+ # the standard input
150
+ # @param [IO] output
151
+ # the standard output
152
+ # @param [Boolean] raise_on_failure
153
+ # whether or not raise on command failure, false by default
154
+ # @param [Boolean] hide_menu
155
+ # whether or not to hide commands menu, false by default
156
+ # @param [Boolean] enable_color
157
+ # disable or force prompt coloring, defaults to nil
158
+ # @param [Symbol] menu_interrupt
159
+ # how to handle Ctrl+C key interrupt out of :error, :signal, :exit, :noop
105
160
  #
106
161
  # @api public
107
- def initialize(*args, **options)
108
- @filename = args.unshift.first
109
- @env = options.fetch(:env) { {} }
110
- @command = options[:command]
111
- if @filename
112
- if ::File.exist?(@filename) && !::FileTest.file?(@filename)
113
- raise ArgumentError, "Don't know how to handle `#{@filename}`. " \
114
- "Please provida a file path or content"
115
- elsif ::File.exist?(@filename) && !options[:content].to_s.empty?
116
- ::File.open(@filename, 'a') { |f| f.write(options[:content]) }
117
- elsif !::File.exist?(@filename)
118
- ::File.write(@filename, options[:content])
119
- end
120
- elsif options[:content]
121
- @filename = tempfile_path(options[:content])
122
- end
162
+ def initialize(command: nil, raise_on_failure: false, hide_menu: false,
163
+ prompt: "Select an editor?", env: {}, enable_color: nil,
164
+ input: $stdin, output: $stdout, menu_interrupt: :error,
165
+ &block)
166
+ @env = env
167
+ @command = nil
168
+ @input = input
169
+ @output = output
170
+ @raise_on_failure = raise_on_failure
171
+ @enable_color = enable_color
172
+ @hide_menu = hide_menu
173
+ @prompt = prompt
174
+ @menu_interrupt = menu_interrupt
175
+
176
+ block.(self) if block
177
+
178
+ command(*Array(command))
123
179
  end
124
180
 
125
181
  # Read or update environment vars
126
182
  #
183
+ # @example
184
+ # editor.env({"FOO" => "bar"})
185
+ #
186
+ # @param [Hash{String => String}] value
187
+ # the environment variables to use
188
+ #
189
+ # @return [Hash]
190
+ #
127
191
  # @api public
128
192
  def env(value = (not_set = true))
129
193
  return @env if not_set
194
+
130
195
  @env = value
131
196
  end
132
197
 
133
- # Finds command using a configured command(s) or detected shell commands.
198
+ # Finds command using a configured command(s) or detected shell commands
134
199
  #
135
- # @param [Array[String]] commands
200
+ # @example
201
+ # editor.command("vim")
202
+ #
203
+ # @param [Array<String>] commands
136
204
  # the optional command to use, by default auto detecting
137
205
  #
138
- # @raise [TTY::CommandInvocationError]
206
+ # @raise [TTY::Editor::CommandInvocationError]
139
207
  #
140
208
  # @return [String]
141
209
  #
@@ -146,64 +214,120 @@ module TTY
146
214
  execs = self.class.available(*commands)
147
215
  if execs.empty?
148
216
  raise EditorNotFoundError,
149
- 'Could not find editor to use. Please specify $VISUAL or $EDITOR'
217
+ "could not find a text editor to use. Please specify $VISUAL or "\
218
+ "$EDITOR or install one of the following editors: " \
219
+ "#{EXECUTABLES.map { |ed| ed.split.first }.join(", ")}."
150
220
  end
151
- exec = choose_exec_from(execs)
152
- @command = TTY::Which.which(exec.to_s)
221
+ @command = choose_exec_from(execs)
153
222
  end
154
223
 
224
+ # Run editor command in a shell
225
+ #
226
+ # @param [Array<String>] files
227
+ # the files to open in an editor
228
+ # @param [String] text
229
+ # the text to edit in an editor
230
+ #
231
+ # @raise [TTY::Editor::CommandInvocationError]
232
+ #
233
+ # @return [Boolean]
234
+ # whether editor command suceeded or not
235
+ #
155
236
  # @api private
156
- def choose_exec_from(execs)
157
- if execs.size > 1
158
- prompt = TTY::Prompt.new
159
- prompt.enum_select('Select an editor?', execs)
160
- else
161
- execs[0]
237
+ def open(*files, text: nil)
238
+ validate_arguments(files, text)
239
+ text_written = false
240
+
241
+ filepaths = files.reduce([]) do |paths, filename|
242
+ if !::File.exist?(filename)
243
+ ::File.write(filename, text || "")
244
+ text_written = true
245
+ end
246
+ paths + [filename]
247
+ end
248
+
249
+ if !text.nil? && !text_written
250
+ tempfile = create_tempfile(text)
251
+ filepaths << tempfile.path
162
252
  end
253
+
254
+ run(filepaths)
255
+ ensure
256
+ tempfile.unlink if tempfile
163
257
  end
164
258
 
165
- # Escape file path
259
+ private
260
+
261
+ # Run editor command with file arguments
262
+ #
263
+ # @param [Array<String>] filepaths
264
+ # the file paths to open in an editor
265
+ #
266
+ # @return [Boolean]
267
+ # whether command succeeded or not
166
268
  #
167
269
  # @api private
168
- def escape_file
169
- Shellwords.shellescape(@filename)
270
+ def run(filepaths)
271
+ command_path = "#{command} #{filepaths.shelljoin}"
272
+ status = system(env, *Shellwords.split(command_path))
273
+ if @raise_on_failure && !status
274
+ raise CommandInvocationError,
275
+ "`#{command_path}` failed with status: #{$? ? $?.exitstatus : nil}"
276
+ end
277
+ !!status
170
278
  end
171
279
 
172
- # Build command path to invoke
280
+ # Check if filename and text arguments are valid
173
281
  #
174
- # @return [String]
282
+ # @raise [InvalidArgumentError]
283
+ #
284
+ # @return [nil]
175
285
  #
176
286
  # @api private
177
- def command_path
178
- "#{command} #{escape_file}"
287
+ def validate_arguments(files, text)
288
+ return if files.empty?
289
+
290
+ if files.all? { |file| ::File.exist?(file) } && !text.nil?
291
+ raise InvalidArgumentError,
292
+ "cannot give a path to an existing file and text at the same time."
293
+ elsif filename = files.find { |file| ::File.exist?(file) && !::FileTest.file?(file) }
294
+ raise InvalidArgumentError, "don't know how to handle `#{filename}`. " \
295
+ "Please provide a file path or text"
296
+ end
179
297
  end
180
298
 
181
- # Create tempfile with content
299
+ # Create tempfile with text
182
300
  #
183
- # @param [String] content
301
+ # @param [String] text
302
+ #
303
+ # @return [Tempfile]
184
304
  #
185
- # @return [String]
186
305
  # @api private
187
- def tempfile_path(content)
188
- tempfile = Tempfile.new('tty-editor')
189
- tempfile << content
306
+ def create_tempfile(text)
307
+ tempfile = Tempfile.new("tty-editor")
308
+ tempfile << text
190
309
  tempfile.flush
191
- unless tempfile.nil?
192
- tempfile.close
193
- end
194
- tempfile.path
310
+ tempfile.close
311
+ tempfile
195
312
  end
196
313
 
197
- # Inovke editor command in a shell
314
+ # Render an editor selection prompt to the terminal
198
315
  #
199
- # @raise [TTY::CommandInvocationError]
316
+ # @return [String]
317
+ # the chosen editor
200
318
  #
201
319
  # @api private
202
- def open
203
- status = system(env, *Shellwords.split(command_path))
204
- return status if status
205
- fail CommandInvocationError,
206
- "`#{command_path}` failed with status: #{$? ? $?.exitstatus : nil}"
320
+ def choose_exec_from(execs)
321
+ if !@hide_menu && execs.size > 1
322
+ prompt = TTY::Prompt.new(input: @input, output: @output, env: @env,
323
+ enable_color: @enable_color,
324
+ interrupt: @menu_interrupt)
325
+ exec = prompt.enum_select(@prompt, execs)
326
+ @output.print(prompt.cursor.up + prompt.cursor.clear_line)
327
+ exec
328
+ else
329
+ execs[0]
330
+ end
207
331
  end
208
332
  end # Editor
209
333
  end # TTY
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TTY
2
4
  class Editor
3
- VERSION = '0.4.0'.freeze
5
+ VERSION = "0.7.0"
4
6
  end # Editor
5
7
  end # TTY
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tty-editor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Murach
8
- autorequire:
9
- bindir: exe
8
+ autorequire:
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-14 00:00:00.000000000 Z
11
+ date: 2021-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tty-prompt
@@ -16,112 +16,69 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.16.0
19
+ version: '0.22'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.16.0
26
+ version: '0.22'
27
27
  - !ruby/object:Gem::Dependency
28
- name: tty-which
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: 0.3.0
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: 0.3.0
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
28
+ name: rake
43
29
  requirement: !ruby/object:Gem::Requirement
44
30
  requirements:
45
31
  - - ">="
46
32
  - !ruby/object:Gem::Version
47
- version: 1.5.0
48
- - - "<"
49
- - !ruby/object:Gem::Version
50
- version: '2.0'
33
+ version: '0'
51
34
  type: :development
52
35
  prerelease: false
53
36
  version_requirements: !ruby/object:Gem::Requirement
54
37
  requirements:
55
38
  - - ">="
56
39
  - !ruby/object:Gem::Version
57
- version: 1.5.0
58
- - - "<"
59
- - !ruby/object:Gem::Version
60
- version: '2.0'
61
- - !ruby/object:Gem::Dependency
62
- name: rake
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '10.0'
68
- type: :development
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '10.0'
40
+ version: '0'
75
41
  - !ruby/object:Gem::Dependency
76
42
  name: rspec
77
43
  requirement: !ruby/object:Gem::Requirement
78
44
  requirements:
79
- - - "~>"
45
+ - - ">="
80
46
  - !ruby/object:Gem::Version
81
47
  version: '3.0'
82
48
  type: :development
83
49
  prerelease: false
84
50
  version_requirements: !ruby/object:Gem::Requirement
85
51
  requirements:
86
- - - "~>"
52
+ - - ">="
87
53
  - !ruby/object:Gem::Version
88
54
  version: '3.0'
89
- description: Opens a file or text in the user's preferred editor.
55
+ description: Open a file or text in a preferred terminal text editor.
90
56
  email:
91
- - ''
57
+ - piotr@piotrmurach.com
92
58
  executables: []
93
59
  extensions: []
94
- extra_rdoc_files: []
60
+ extra_rdoc_files:
61
+ - README.md
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
95
64
  files:
96
- - ".gitignore"
97
- - ".rspec"
98
- - ".travis.yml"
99
65
  - CHANGELOG.md
100
- - CODE_OF_CONDUCT.md
101
- - Gemfile
102
66
  - LICENSE.txt
103
67
  - README.md
104
- - Rakefile
105
- - appveyor.yml
106
- - bin/console
107
- - bin/setup
108
- - examples/basic.rb
109
- - examples/choices.rb
110
- - examples/empty.rb
111
- - examples/env.rb
112
- - examples/tempfile.rb
113
68
  - lib/tty-editor.rb
114
69
  - lib/tty/editor.rb
115
70
  - lib/tty/editor/version.rb
116
- - tasks/console.rake
117
- - tasks/coverage.rake
118
- - tasks/spec.rake
119
- - tty-editor.gemspec
120
- homepage: https://piotrmurach.github.io/tty
71
+ homepage: https://ttytoolkit.org
121
72
  licenses:
122
73
  - MIT
123
- metadata: {}
124
- post_install_message:
74
+ metadata:
75
+ allowed_push_host: https://rubygems.org
76
+ bug_tracker_uri: https://github.com/piotrmurach/tty-editor/issues
77
+ changelog_uri: https://github.com/piotrmurach/tty-editor/blob/master/CHANGELOG.md
78
+ documentation_uri: https://www.rubydoc.info/gems/tty-editor
79
+ homepage_uri: https://ttytoolkit.org
80
+ source_code_uri: https://github.com/piotrmurach/tty-editor
81
+ post_install_message:
125
82
  rdoc_options: []
126
83
  require_paths:
127
84
  - lib
@@ -136,9 +93,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
93
  - !ruby/object:Gem::Version
137
94
  version: '0'
138
95
  requirements: []
139
- rubyforge_project:
140
- rubygems_version: 2.5.1
141
- signing_key:
96
+ rubygems_version: 3.1.2
97
+ signing_key:
142
98
  specification_version: 4
143
- summary: Opens a file or text in the user's preferred editor.
99
+ summary: Open a file or text in a preferred terminal text editor.
144
100
  test_files: []