Class: Alda::REPL
- Inherits:
-
Object
- Object
- Alda::REPL
- Defined in:
- lib/alda-rb/repl.rb
Overview
An instance of this class is an REPL session.
It provides an Alda::REPL::TempScore for you to operate on. To see what methods you can call in an REPL session, see instance methods of Alda::REPL::TempScore.
The session uses “> ” to indicate your input. Your input should be ruby codes, and the codes will be sent to an Alda::REPL::TempScore and executed.
After executing the ruby codes, if the score is not empty, it is played, and the translated alda codes are printed.
Note that every time your ruby codes input is executed, the score is cleared beforehand. To check the result of your previous input, run puts history
.
Unlike IRB, this REPL does not print the result of the executed codes. Use p
or puts
if you want.
Interrupt
and SystemExit
exceptions are rescued and will not cause the process terminating. exit
terminates the REPL session instead of the process.
To start an REPL session in a ruby program, use #run. To start an REPL session conveniently from command line, run command alda-irb
. For details about this command line tool, run alda-irb --help
.
$ alda-irb
> p processes.last
{:id=>"dus", :port=>34317, :state=>nil, :expiry=>nil, :type=>:repl_server}
> piano_; c d e f
piano: [c d e f]
> 5.times do
. c
> end
c c c c c
> score_text
piano: [c d e f]
c c c c c
> play
Playing...
> save 'temp.alda'
> puts `cat temp.alda`
piano: [c d e f]
c c c c c
> system 'rm temp.alda'
> exit
Notice that there is a significant difference between Alda 1 REPL and Alda 2 REPL. In short, Alda 2 has a much more powerful REPL than Alda 1, so it dropped the --history
option in the alda play
command line interface (alda-lang/alda#367). It has an nREPL server, and this class simply functions by sending messages to the nREPL server. However, for Alda 1, this class maintains necessary information in the memory of the Ruby program, and the REPL is implemented by repeatedly running alda play
in command line. Therefore, this class functions differently for Alda 1 and Alda 2 and you thus should not modify Alda::generation during an REPL session.
It is also possible to use this class as a Ruby wrapper of APIs of the Alda nREPL server in Alda 2. In this usage, you never need to call #run, and you call #message or #raw_message instead.
repl = Alda::REPL.new
repl. :eval_and_play, code: 'piano: c d e f' # => nil
repl. :eval_and_play, code: 'g a b > c' # => nil
repl. :score_text # => "piano: [c d e f]\ng a b > c\n"
repl. :eval_and_play, code: 'this will cause an error' # (raises Alda::NREPLServerError)
Defined Under Namespace
Classes: TempScore
Instance Attribute Summary collapse
-
#color ⇒ Object
Whether the output should be colored.
-
#host ⇒ Object
readonly
The host of the nREPL server.
-
#port ⇒ Object
readonly
The port of the nREPL server.
-
#preview ⇒ Object
Whether a preview of what Alda code will be played everytime you input ruby codes.
-
#reline ⇒ Object
Whether to use Reline for input.
Instance Method Summary collapse
-
#clear_history ⇒ Object
:call-seq: clear_history() -> nil.
-
#history ⇒ Object
:call-seq: history() -> String.
-
#initialize(color: true, preview: true, reline: true, **opts) ⇒ REPL
constructor
:call-seq: new(**opts) -> Alda::REPL.
-
#message(op, **params) ⇒ Object
:call-seq: message(op, **params) -> String or Hash.
-
#play_score(code) ⇒ Object
:call-seq: play_score(code) -> nil.
-
#process_rb_code(code) ⇒ Object
:call-seq: process_rb_code(code) -> true or false.
-
#raw_message(contents) ⇒ Object
:call-seq: raw_message(contents) -> Hash.
-
#rb_code ⇒ Object
:call-seq: rb_code() -> String.
-
#readline(indent = 0) ⇒ Object
:call-seq: readline(indent = 0) -> String.
-
#run ⇒ Object
:call-seq: run() -> nil.
-
#setup_repl(opts) ⇒ Object
:call-seq: setup_repl(opts) -> nil.
-
#start ⇒ Object
:call-seq: start() -> nil.
-
#terminate ⇒ Object
:call-seq: terminate() -> nil.
-
#try_command ⇒ Object
:call-seq: try_command() { … } -> obj.
Constructor Details
#initialize(color: true, preview: true, reline: true, **opts) ⇒ REPL
:call-seq:
new(**opts) -> Alda::REPL
Creates a new Alda::REPL. The parameter color
specifies whether the output should be colored (sets #color). The parameter preview
specifies whether a preview of what Alda code will be played everytime you input ruby codes (sets #preview). The parameter reline
specifies whether to use Reline for input.
The opts
are passed to the command line of alda repl
. Available options are host
, port
, etc. Run alda repl --help
for more info. If port
is specified and host
is not or is specified to be "localhost"
or "127.0.0.1"
, then this method will try to connect to an existing Alda REPL server. A new one will be started only if no existing server is found.
The opts
are ignored in Alda 1.
249 250 251 252 253 254 255 256 257 258 |
# File 'lib/alda-rb/repl.rb', line 249 def initialize color: true, preview: true, reline: true, **opts @score = TempScore.new self @binding = @score.get_binding # IRB once changed the API of RubyLex#initialize. Take care of that. @lex = RubyLex.new *(RubyLex.instance_method(:initialize).arity == 0 ? [] : [@binding]) @color = color @preview = preview @reline = reline setup_repl opts end |
Instance Attribute Details
#color ⇒ Object
Whether the output should be colored.
219 220 221 |
# File 'lib/alda-rb/repl.rb', line 219 def color @color end |
#host ⇒ Object (readonly)
The host of the nREPL server. Only useful in Alda 2.
211 212 213 |
# File 'lib/alda-rb/repl.rb', line 211 def host @host end |
#port ⇒ Object (readonly)
The port of the nREPL server. Only useful in Alda 2.
215 216 217 |
# File 'lib/alda-rb/repl.rb', line 215 def port @port end |
#preview ⇒ Object
Whether a preview of what Alda code will be played everytime you input ruby codes.
223 224 225 |
# File 'lib/alda-rb/repl.rb', line 223 def preview @preview end |
#reline ⇒ Object
Whether to use Reline for input. When it is false, the REPL session will be less buggy but less powerful.
228 229 230 |
# File 'lib/alda-rb/repl.rb', line 228 def reline @reline end |
Instance Method Details
#clear_history ⇒ Object
:call-seq:
clear_history() -> nil
In Alda 1, clears #history. In Alda 2, askes the nREPL server to clear its history (start a new score).
506 507 508 509 510 511 512 513 |
# File 'lib/alda-rb/repl.rb', line 506 def clear_history if Alda.v1? @history = StringIO.new else try_command { :new_score } end nil end |
#history ⇒ Object
:call-seq:
history() -> String
In Alda 1, it is the same as an attribute reader. In Alda 2, it asks the nREPL server for its score text and returns it.
492 493 494 495 496 497 498 |
# File 'lib/alda-rb/repl.rb', line 492 def history if Alda.v1? @history else try_command { :score_text } end end |
#message(op, **params) ⇒ Object
:call-seq:
message(op, **params) -> String or Hash
Sends a message to the nREPL server with the following format, with op
being the operation name (the op
field in the message), and params
being the parameters (other fields in the message). Then, this method analyzes the response. If there is an error, raises Alda::NREPLServerError. Otherwise, if the response contains only one field, return the content of that field (a String). Otherwise, return the whole response as a Hash.
repl = Alda::REPL.new
repl. :eval_and_play, code: 'piano: c d e f' # => nil
repl. :eval_and_play, code: 'g a b > c' # => nil
repl. :score_text # => "piano: [c d e f]\ng a b > c\n"
repl. :eval_and_play, code: 'this will cause an error' # (raises Alda::NREPLServerError)
320 321 322 323 324 325 326 327 328 329 330 331 |
# File 'lib/alda-rb/repl.rb', line 320 def op, **params result = op: Alda::Utils.snake_to_slug(op), **params result.transform_keys! { Alda::Utils.slug_to_snake _1 } if (status = result.delete :status).include? 'error' raise Alda::NREPLServerError.new @host, @port, result.delete(:problems), status end case result.size when 0 then nil when 1 then result.values.first else result end end |
#play_score(code) ⇒ Object
:call-seq:
play_score(code) -> nil
Appends code
to the history and plays the code
as Alda code. In Alda 1, plays the score by sending code
to command line alda. In Alda 2, sends code
to the nREPL server for evaluating and playing.
452 453 454 455 456 457 458 459 |
# File 'lib/alda-rb/repl.rb', line 452 def play_score code if Alda.v1? Alda.play code: code, history: @history @history.puts code else :eval_and_play, code: code end end |
#process_rb_code(code) ⇒ Object
:call-seq:
process_rb_code(code) -> true or false
Processes the Ruby codes read. Sends it to a score and sends the result to command line alda. Returns false
for breaking the REPL main loop, true
otherwise.
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 |
# File 'lib/alda-rb/repl.rb', line 411 def process_rb_code code @score.clear begin @binding.eval code rescue StandardError, ScriptError, Interrupt => e $stderr.print e. return true rescue SystemExit return false end code = @score.events_alda_codes unless code.empty? $stdout.puts @color ? code.yellow : code try_command { play_score code } end true end |
#raw_message(contents) ⇒ Object
296 297 298 299 300 301 |
# File 'lib/alda-rb/repl.rb', line 296 def contents Alda::GenerationError.assert_generation [:v2] contents = JSON.parse contents if contents.is_a? String @socket.write contents.bencode @bencode_parser.parse! end |
#rb_code ⇒ Object
:call-seq:
rb_code() -> String
Reads and returns the next Ruby codes input in the REPL session. It can intelligently continue reading if the code is not complete yet.
362 363 364 365 366 367 368 369 370 371 372 373 374 375 |
# File 'lib/alda-rb/repl.rb', line 362 def rb_code result = '' indent = 0 begin result.concat readline(indent).tap { return unless _1 }, ?\n # IRB once changed the API of RubyLex#check_state. Take care of that. opts = @lex.method(:check_state).arity.positive? ? {} : { context: @binding } ltype, indent, continue, block_open = @lex.check_state result, **opts rescue Interrupt $stdout.puts return '' end while ltype || indent.nonzero? || continue || block_open result end |
#readline(indent = 0) ⇒ Object
:call-seq:
readline(indent = 0) -> String
Prompts the user to input a line. The parameter indent
is the indentation level. Twice the number of spaces is already in the input field before the user fills in if #reline is true. The prompt hint is different for zero indent
and nonzero indent
. Returns the user input.
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 |
# File 'lib/alda-rb/repl.rb', line 387 def readline indent = 0 prompt = indent.nonzero? ? '. ' : '> ' prompt = prompt.green if @color if @reline Reline.pre_input_hook = -> do Reline.insert_text ' ' * indent Reline.redisplay Reline.pre_input_hook = nil end Reline.readline prompt, true else $stdout.print prompt $stdout.flush $stdin.gets chomp: true end end |
#run ⇒ Object
:call-seq:
run() -> nil
Runs the session. Includes the start (#start), the main loop, and the termination (#terminate).
339 340 341 342 343 344 345 346 |
# File 'lib/alda-rb/repl.rb', line 339 def run start while code = rb_code next if code.empty? break unless process_rb_code code end terminate end |
#setup_repl(opts) ⇒ Object
:call-seq:
setup_repl(opts) -> nil
Sets up the REPL session. This method is called in ::new. After you #terminate the session, you cannot use the REPL anymore unless you call this method again.
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/alda-rb/repl.rb', line 268 def setup_repl opts if Alda.v1? @history = StringIO.new else @port = (opts.fetch :port, -1).to_i @host = opts.fetch :host, 'localhost' unless @port.positive? && %w[localhost 127.0.0.1].include?(@host) && Alda.processes.any? { _1[:port] == @port && _1[:type] == :repl_server } Alda.env(ALDA_DISABLE_SPAWNING: :no) { @nrepl_pipe = Alda.pipe :repl, **opts, server: true } /nrepl:\/\/[a-zA-Z0-9._\-]+:(?<port>\d+)/ =~ @nrepl_pipe.gets @port = port.to_i Process.detach @nrepl_pipe.pid end @socket = TCPSocket.new @host, @port @bencode_parser = BEncode::Parser.new @socket end nil end |
#start ⇒ Object
:call-seq:
start() -> nil
Starts the session. Currently does nothing.
353 354 |
# File 'lib/alda-rb/repl.rb', line 353 def start end |
#terminate ⇒ Object
:call-seq:
terminate() -> nil
Terminates the REPL session. In Alda 1, just calls #clear_history. In Alda 2, sends a SIGINT to the nREPL server if it was spawned by the Ruby program.
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 |
# File 'lib/alda-rb/repl.rb', line 468 def terminate if Alda.v1? clear_history else if @nrepl_pipe if Alda::Utils.win_platform? unless IO.popen(['taskkill', '/f', '/pid', @nrepl_pipe.pid.to_s], &:read).include? 'SUCCESS' Alda::Warning.warn 'failed to kill nREPL server; may become zombie process' end else Process.kill :INT, @nrepl_pipe.pid end @nrepl_pipe.close end @socket.close end end |
#try_command ⇒ Object
:call-seq:
try_command() { ... } -> obj
Run the block. In Alda 1, catches Alda::CommandLineError. In Alda 2, catches Alda::NREPLServerError. If an error is caught, prints the error message (in red if #color is true).
437 438 439 440 441 442 443 |
# File 'lib/alda-rb/repl.rb', line 437 def try_command begin yield rescue Alda.v1? ? Alda::CommandLineError : Alda::NREPLServerError => e puts @color ? e..red : e. end end |