tty-editor 0.4.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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: []