## # Adding functions that is accessible everywhere. module Kernel ## # :call-seq: # alda(*args) -> true or false # # Runs the alda command. # Does not capture output. # # alda 'version' # alda 'play', '-c', 'piano: a' # alda 'repl' # # Returns whether the exit status is +0+. def alda *args system Alda.executable, *args end end module Alda ## # The Array of possible values of ::generation. # It is just the array [:v1, :v2] # # You can use +:v1?+ and +:v2?+ to get whether the current generation is +:v1+ or +:v2+. # For example, Alda.v1? is the same as Alda.generation == :v1. # You can also use +:v1!+ and +:v2!+ to set the generation to +:v1+ or +:v2+. # For example, Alda.v1! is the same as Alda.generation = :v1. GENERATIONS = %i[v1 v2].freeze GENERATIONS.each do |gen| module_function define_method("#{gen}?") { @generation == gen } module_function define_method("#{gen}!") { @generation = gen } end ## # The available subcommands of alda executable. # This is a Hash, with keys being possible values of ::generation, # and values being an Array of symbols of the available commands of that generation. # # Alda is able to invoke +alda+ at the command line. # The subcommand is the name of the method invoked upon Alda. # # The keyword arguments are interpreted as the subcommand options. # To specify the command options, use ::[]. # # The return value is the string output by the command in STDOUT. # # If the exit code is nonzero, an Alda::CommandLineError is raised. # # Alda.version # # => "Client version: 1.4.0\nServer version: [27713] 1.4.0\n" # Alda.parse code: 'bassoon: o3 c' # # => "{\"chord-mode\":false,\"current-instruments\":...}\n" # # The available commands are: # # * If ::generation is +:v1+: # +help+, +update+, +repl+, +up+, +start_server+, +init+, +down+, +stop_server+, # +downup+, +restart_server+, +list+, +status+, +version+, +play+, +stop+, +parse+, # +instruments+, and +export+. # * If ::generation is +:v2+: # +doctor+, +export+, +help+, +import+, +instruments+, +parse+, +play+, # +ps+, +repl+, +shutdown+, +stop+, +telemetry+, +update+, and +version+. # # Trying to run a command that is not support by the current generation set by ::generation # will raise an Alda::GenerationError. COMMANDS_FOR_VERSIONS = { v1: %i[ help update repl up start_server init down stop_server downup restart_server list status version play stop parse instruments export ].freeze, v2: %i[ doctor export help import instruments parse play ps repl shutdown stop telemetry update version ].freeze }.freeze ## # The Hash of available commands. # The symbols of commands are keys # and each value is an Array of generations where the command is available. COMMANDS = COMMANDS_FOR_VERSIONS.each_with_object({}) do |(gen, commands), r| commands.each { (r[_1] ||= []).push gen } end.freeze COMMANDS.each do |command, generations| define_method command do |*args, **opts| Alda::GenerationError.assert_generation generations result = Alda.pipe command, *args, **opts, &:read raise CommandLineError.new $?, result if $?.exitstatus.nonzero? result end.tap { module_function _1 } end class << self ## # The path to the +alda+ executable. # # The default value is "alda", # which will depend on your +PATH+. attr_accessor :executable ## # The commandline options set using ::[]. # Not the subcommand options. # Clear it using ::clear_options. attr_reader :options ## # The major version of the +alda+ command used. # Possible values: +:v1+ or +:v2+ (i.e. one of the values in Alda::GENERATIONS). # If you try to specify it to values other than those, an ArgumentError will be raised. # This affects several things due to some incompatible changes from \Alda 1 to \Alda 2. # You may use ::deduce_generation to automatically set it, # or use #v1! or #v2! to set it in a shorter way. attr_accessor :generation def generation= gen # :nodoc: raise ArgumentError, "bad generation: #{gen}" unless GENERATIONS.include? gen @generation = gen end ## # :call-seq: # Alda[**opts] -> self # # Sets the options of alda command. # Not the subcommand options. # # # This example only works for Alda 1. # Alda[port: 1108].up # => "[1108] ..." # Alda.status # => "[1108] ..." # # Further set options will be merged. # The options can be seen by ::options. # To clear them, use ::clear_options. def [] **opts @options.merge! opts self end ## # :call-seq: # clear_options() -> nil # # Clears the command line options. # Makes ::options an empty Array. def clear_options @options.clear end end @executable = 'alda' @options = {} @env = { 'ALDA_DISABLE_SPAWNING' => 'yes', 'ALDA_DISABLE_TELEMETRY' => 'yes' } v2! ## # :call-seq: # env() -> Hash # env(hash) -> Hash # env(hash) { ... } -> Object # # When called with no arguments, # returns the commandline environment variables (a Hash) # used when running +alda+ on command line. # It is {"ALDA_DISABLE_SPAWNING"=>"yes","ALDA_DISABLE_TELEMETRY"=>"yes"} by default # (for speeding up the command line responses: # {alda-lang/alda#368}[https://github.com/alda-lang/alda/issues/368]). # # When called with an argument +hash+, # merge the old environment variables with +hash+ and set # the merged Hash as the new environment variables. # Returns the new environment variables (a Hash). # # When called with an argument +hash+ and a block, # execute the block with the environment being set to the merge of the old environment # and +hash+, and then restore the old environment. # Returns the returned value of the block. def env hash = nil, &block if hash @env = (old_env = @env).merge hash.map { |k, v| [k.to_s, v.to_s] }.to_h block ? block.().tap { @env = old_env } : @env else @env end end ## # :call-seq: # pipe(command, *args, **opts) -> IO # pipe(command, *args, **opts) { |io| ... } -> Object # # Runs +alda+ in command line as a child process and returns the pipe IO # or pass the IO to the block. # See COMMANDS_FOR_VERSIONS for an explanation of +args+ and +opts+. def pipe command, *args, **opts, &block add_option = ->((key, val)) do next unless val args.push "--#{Alda::Utils.snake_to_slug key}" args.push val.to_s unless val == true end # executable args.unshift Alda.executable args.map! &:to_s # options Alda.options.each &add_option # subcommand args.push command.to_s # subcommand options opts.each &add_option # subprocess spawn_options = Alda::Utils.win_platform? ? { new_pgroup: true } : { pgroup: true } IO.popen Alda.env, args, **spawn_options, &block end ## # :call-seq: # processes() -> Array # # Returns a Array of details about running \Alda processes. # Only available for \Alda 2. # Each element in the Array is a Hash, # and each Hash has the following keys: # - +:id+: the player-id of the process, a three-letter String. # - +:port+: the port number of the process, an Integer. # - +:state+: the state of the process, a Symbol (may be +nil+, +:ready+, +:active+, or +:starting+). # - +:expiry+: a human-readable description of expiry time of the process, a String (may be +nil+). # - +:type+: the type of the process, a Symbol (may be +:player+ or +:repl_server+). def processes raise GenerationError.new [:v2] if v1? Alda.ps.lines(chomp: true)[1..].map do |line| id, port, state, expiry, type = line.split ?\t port = port.to_i state = state == ?- ? nil : state.to_sym expiry = nil if expiry == ?- type = Alda::Utils.slug_to_snake type { id: id, port: port, state: state, expiry: expiry, type: type } end end ## # :call-seq: # up?() -> true or false # # Whether the alda server is up. # Checks whether there are any play processes in ::processes in \Alda 2. def up? Alda.v1? ? Alda.status.include?('up') : Alda.processes.any? { _1[:type] == :player } end ## # :call-seq: # down? -> true or false # # Whether the alda server is down. # Checks whether there are no play processes in ::processes in \Alda 2. def down? Alda.v1? ? Alda.status.include?('down') : Alda.processes.none? { _1[:type] == :player } end ## # :call-seq: # deduce_generation -> one of Alda::GENERATIONS # # Deduce the generation of \Alda being used by running alda version in command line, # and then set ::generation accordingly. def deduce_generation /(?\d+)\.(?\d+)\.(?\d+)/ =~ Alda.version @generation = major == '1' ? :v1 : :v2 end module_function :env, :pipe, :processes, :up?, :down?, :deduce_generation end