lib/tty/editor.rb in tty-editor-0.5.1 vs lib/tty/editor.rb in tty-editor-0.6.0
- old
+ new
@@ -1,134 +1,155 @@
# frozen_string_literal: true
-require 'tty-prompt'
-require 'tty-which'
-require 'tempfile'
-require 'fileutils'
-require 'shellwords'
+require "fileutils"
+require "shellwords"
+require "tempfile"
+require "tty-prompt"
-require_relative 'editor/version'
+require_relative "editor/version"
module TTY
# A class responsible for launching an editor
#
# @api public
class Editor
+ Error = Class.new(StandardError)
+
+ # Raised when user provides unnexpected or incorrect argument
+ InvalidArgumentError = Class.new(Error)
+
# Raised when command cannot be invoked
class CommandInvocationError < RuntimeError; end
# Raised when editor cannot be found
class EditorNotFoundError < RuntimeError; end
- # Check if editor exists
+ # List possible command line text editors
#
- # @return [Boolean]
+ # @return [Array[String]]
#
- # @api private
- def self.exist?(cmd)
- TTY::Which.exist?(cmd)
- end
+ # @api public
+ EXECUTABLES = [
+ "nano -w", "notepad", "vim", "vi", "emacs",
+ "code", "subl -n -w", "mate -w", "atom",
+ "pico", "qe", "mg", "jed"
+ ].freeze
- # Check if Windowz
+ # Check if editor command exists
#
+ # @example
+ # exist?("vim") # => true
+ #
# @return [Boolean]
#
- # @api public
- def self.windows?
- ::File::ALT_SEPARATOR == "\\"
+ # @api private
+ def self.exist?(command)
+ exts = ENV.fetch("PATHEXT", "").split(::File::PATH_SEPARATOR)
+ ENV.fetch("PATH", "").split(::File::PATH_SEPARATOR).any? do |dir|
+ file = ::File.join(dir, command)
+ ::File.exist?(file) || exts.any? { |ext| ::File.exist?("#{file}#{ext}") }
+ end
end
# Check editor from environment variables
#
# @return [Array[String]]
#
# @api public
def self.from_env
- [ENV['VISUAL'], ENV['EDITOR']].compact
+ [ENV["VISUAL"], ENV["EDITOR"]].compact
end
- # List possible executable for editor command
+ # Find available text editors
#
- # @return [Array[String]]
- #
- # @api public
- def self.executables
- ['vim', 'vi', 'emacs', 'nano', 'nano-tiny', 'pico', 'mate -w']
- end
-
- # Find available command
- #
# @param [Array[String]] commands
# the commands to use intstead of defaults
#
# @return [Array[String]]
+ # the existing editor commands
#
# @api public
def self.available(*commands)
- return commands unless commands.empty?
-
- if !from_env.all?(&:empty?)
- [from_env.find { |e| !e.empty? }]
- elsif windows?
- ['notepad']
- else
- executables.uniq.select(&method(:exist?))
- end
+ execs = if !commands.empty?
+ commands.map(&:to_s)
+ elsif from_env.any?
+ [from_env.first]
+ else
+ EXECUTABLES
+ end
+ execs.compact.map(&:strip).reject(&:empty?).uniq
+ .select { |exec| exist?(exec.split.first) }
end
# Open file in system editor
#
# @example
- # TTY::Editor.open('filename.rb')
+ # TTY::Editor.open("/path/to/filename")
#
- # @param [String] file
- # the name of the file
+ # @example
+ # TTY::Editor.open("file1", "file2", "file3")
#
+ # @example
+ # TTY::Editor.open(text: "Some text")
+ #
+ # @param [Array<String>] files
+ # the files to open in an editor
+ # @param [String] :command
+ # the editor command to use, by default auto detects
+ # @param [String] :text
+ # the text to edit in an editor
+ # @param [Hash] :env
+ # environment variables to forward to the editor
+ #
# @return [Object]
#
# @api public
- def self.open(*args)
- editor = new(*args)
-
- yield(editor) if block_given?
-
- editor.open
+ def self.open(*files, text: nil, **options, &block)
+ editor = new(**options, &block)
+ editor.open(*files, text: text)
end
# Initialize an Editor
#
- # @param [String] file
- # @param [Hash[Symbol]] options
- # @option options [Hash] :command
+ # @param [String] :command
# the editor command to use, by default auto detects
- # @option options [Hash] :env
+ # @param [Hash] :env
# environment variables to forward to the editor
+ # @param [IO] :input
+ # the standard input
+ # @param [IO] :output
+ # the standard output
+ # @param [Boolean] :raise_on_failure
+ # whether or not raise on command failure, false by default
+ # @param [Boolean] :show_menu
+ # whether or not show commands menu, true by default
#
# @api public
- def initialize(*args, **options)
- @filename = args.unshift.first
- @env = options.fetch(:env) { {} }
- @command = options[:command]
- if @filename
- if ::File.exist?(@filename) && !::FileTest.file?(@filename)
- raise ArgumentError, "Don't know how to handle `#{@filename}`. " \
- "Please provida a file path or content"
- elsif ::File.exist?(@filename) && !options[:content].to_s.empty?
- ::File.open(@filename, 'a') { |f| f.write(options[:content]) }
- elsif !::File.exist?(@filename)
- ::File.write(@filename, options[:content])
- end
- elsif options[:content]
- @filename = tempfile_path(options[:content])
- end
+ def initialize(command: nil, raise_on_failure: false, show_menu: true,
+ prompt: "Select an editor?", env: {},
+ input: $stdin, output: $stdout, &block)
+ @env = env
+ @command = nil
+ @input = input
+ @output = output
+ @raise_on_failure = raise_on_failure
+ @show_menu = show_menu
+ @prompt = prompt
+
+ block.(self) if block
+
+ command(*Array(command))
end
# Read or update environment vars
#
+ # @return [Hash]
+ #
# @api public
def env(value = (not_set = true))
return @env if not_set
+
@env = value
end
# Finds command using a configured command(s) or detected shell commands.
#
@@ -144,66 +165,118 @@
return @command if @command && commands.empty?
execs = self.class.available(*commands)
if execs.empty?
raise EditorNotFoundError,
- 'Could not find editor to use. Please specify $VISUAL or $EDITOR'
+ "could not find a text editor to use. Please specify $VISUAL or "\
+ "$EDITOR or install one of the following editors: " \
+ "#{EXECUTABLES.map { |ed| ed.split.first }.join(", ")}."
end
- exec = choose_exec_from(execs)
- @command = TTY::Which.which(exec.to_s)
+ @command = choose_exec_from(execs)
end
+ # Run editor command in a shell
+ #
+ # @param [Array<String>] files
+ # the files to open in an editor
+ # @param [String] :text
+ # the text to edit in an editor
+ #
+ # @raise [TTY::CommandInvocationError]
+ #
+ # @return [Boolean]
+ # whether editor command suceeded or not
+ #
# @api private
- def choose_exec_from(execs)
- if execs.size > 1
- prompt = TTY::Prompt.new
- prompt.enum_select('Select an editor?', execs)
- else
- execs[0]
+ def open(*files, text: nil)
+ validate_arguments(files, text)
+ text_written = false
+
+ filepaths = files.reduce([]) do |paths, filename|
+ if !::File.exist?(filename)
+ ::File.write(filename, text || "")
+ text_written = true
+ end
+ paths + [filename]
end
+
+ if !text.nil? && !text_written
+ tempfile = create_tempfile(text)
+ filepaths << tempfile.path
+ end
+
+ run(filepaths)
+ ensure
+ tempfile.unlink if tempfile
end
- # Escape file path
+ private
+
+ # Run editor command with file arguments
#
+ # @param [Array<String>] filepaths
+ # the file paths to open in an editor
+ #
+ # @return [Boolean]
+ # whether command succeeded or not
+ #
# @api private
- def escape_file
- Shellwords.shellescape(@filename)
+ def run(filepaths)
+ command_path = "#{command} #{filepaths.shelljoin}"
+ status = system(env, *Shellwords.split(command_path))
+ if @raise_on_failure && !status
+ raise CommandInvocationError,
+ "`#{command_path}` failed with status: #{$? ? $?.exitstatus : nil}"
+ end
+ !!status
end
- # Build command path to invoke
+ # Check if filename and text arguments are valid
#
- # @return [String]
+ # @raise [InvalidArgumentError]
#
# @api private
- def command_path
- "#{command} #{escape_file}"
+ def validate_arguments(files, text)
+ return if files.empty?
+
+ if files.all? { |file| ::File.exist?(file) } && !text.nil?
+ raise InvalidArgumentError,
+ "cannot give a path to an existing file and text at the same time."
+ elsif filename = files.find { |file| ::File.exist?(file) && !::FileTest.file?(file) }
+ raise InvalidArgumentError, "don't know how to handle `#{filename}`. " \
+ "Please provide a file path or text"
+ end
end
- # Create tempfile with content
+ # Create tempfile with text
#
- # @param [String] content
+ # @param [String] text
#
- # @return [String]
+ # @return [Tempfile]
+ #
# @api private
- def tempfile_path(content)
- tempfile = Tempfile.new('tty-editor')
- tempfile << content
+ def create_tempfile(text)
+ tempfile = Tempfile.new("tty-editor")
+ tempfile << text
tempfile.flush
- unless tempfile.nil?
- tempfile.close
- end
- tempfile.path
+ tempfile.close
+ tempfile
end
- # Inovke editor command in a shell
+ # Render an editor selection prompt to the terminal
#
- # @raise [TTY::CommandInvocationError]
+ # @return [String]
+ # the chosen editor
#
# @api private
- def open
- status = system(env, *Shellwords.split(command_path))
- return status if status
- fail CommandInvocationError,
- "`#{command_path}` failed with status: #{$? ? $?.exitstatus : nil}"
+ def choose_exec_from(execs)
+ if @show_menu && execs.size > 1
+ prompt = TTY::Prompt.new(input: @input, output: @output, env: @env)
+ exec = prompt.enum_select(@prompt, execs)
+ @output.print(prompt.cursor.up + prompt.cursor.clear_line)
+ exec
+ else
+ execs[0]
+ end
end
end # Editor
end # TTY