Class: Qcmd::CLI

Inherits:
Object
  • Object
show all
Includes:
Plaintext
Defined in:
lib/qcmd/cli.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Plaintext

#ascii_qlab, #centered_text, #columns, #joined_wrapped, #log, #pluralize, #print, #print_wrapped, #right_text, #set_columns, #split_text, #table, #word_wrap, #wrapped_text

Constructor Details

#initialize(options = {}) ⇒ CLI

Returns a new instance of CLI.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/qcmd/cli.rb', line 14

def initialize options={}
  Qcmd.debug "[CLI initialize] launching with options: #{options.inspect}"

  Qcmd.context = Qcmd::Context.new

  if options[:machine_given]
    Qcmd.debug "[CLI initialize] autoconnecting to machine #{ options[:machine] }"

    Qcmd.while_quiet do
      connect_to_machine_by_name(options[:machine])
    end

    load_workspaces

    if options[:workspace_given]
      Qcmd.debug "[CLI initialize] autoconnecting to workspace #{ options[:workspace] }"

      Qcmd.while_quiet do
        connect_to_workspace_by_name(options[:workspace], options[:workspace_passcode])
      end

      if options[:command_given]
        split_and_handle options[:command]
        exit
      end
    elsif !connect_default_workspace
      Handler.print_workspace_list
      # end
    end
  end

  # add aliases to input completer
  InputCompleter.add_commands aliases.keys

  self
end

Instance Attribute Details

#promptObject

Returns the value of attribute prompt.



8
9
10
# File 'lib/qcmd/cli.rb', line 8

def prompt
  @prompt
end

Class Method Details

.launch(options = {}) ⇒ Object



10
11
12
# File 'lib/qcmd/cli.rb', line 10

def self.launch options={}
  new(options).start
end

Instance Method Details

#add_alias(name, expression) ⇒ Object



67
68
69
70
71
72
73
# File 'lib/qcmd/cli.rb', line 67

def add_alias name, expression
  aliases[name] = Parser.generate(expression)
  InputCompleter.add_command name
  Qcmd::Configuration.update('aliases', aliases)

  aliases[name]
end

#add_cues_to_list(cue, list, level) ⇒ Object



654
655
656
657
658
659
660
661
662
663
664
665
# File 'lib/qcmd/cli.rb', line 654

def add_cues_to_list cue, list, level
  cue.cues.each {|_c|
    name = _c.name

    if level > 0
      name += " " + ("-" * level) + "|"
    end

    list << [_c.number, _c.id, name, _c.type]
    add_cues_to_list(_c, list, level + 1) if _c.has_cues?
  }
end

#alias_arg_matcherObject



63
64
65
# File 'lib/qcmd/cli.rb', line 63

def alias_arg_matcher
  /\$(\d+)/
end

#aliasesObject



59
60
61
# File 'lib/qcmd/cli.rb', line 59

def aliases
  @aliases ||= Qcmd::Aliases.defaults.merge(Qcmd::Configuration.config['aliases'] || {})
end

#connect_machine(machine) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/qcmd/cli.rb', line 159

def connect_machine machine
  if machine.nil?
    print "A valid machine is needed to connect!"
    return
  end

  reset

  Qcmd.context.machine = machine

  # in case this is a reconnection
  Qcmd.context.connect_to_qlab

  # tell QLab to always reply to messages
  response = Qcmd::Action.evaluate('/alwaysReply 1')
  if response.nil? || response.to_s.empty?
    log(:error, %[Failed to connect to QLab machine "#{ machine.name }"])
  elsif response.status == 'ok'
    print %[Connected to machine "#{ machine.name }"]
  end
end

#connect_to_machine_by_index(machine_idx) ⇒ Object



221
222
223
224
225
226
227
228
# File 'lib/qcmd/cli.rb', line 221

def connect_to_machine_by_index machine_idx
  if machine = Qcmd::Network.find_by_index(machine_idx)
    print "Connecting to machine: #{machine.name}"
    connect_machine machine
  else
    log(:warning, 'Sorry, that machine could not be found')
  end
end

#connect_to_machine_by_name(machine_name) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/qcmd/cli.rb', line 192

def connect_to_machine_by_name machine_name
  machine = nil

  # machine name can be found or IPv4 address is given

  if machine_name.nil? || machine_name.to_s.empty?
    machine = nil
  elsif Qcmd::Network.find(machine_name)
    log(:debug, "[connect_to_machine_by_name] Searching for machine by name: #{ machine_name.to_s }")
    machine = Qcmd::Network.find(machine_name)
  elsif Qcmd::Network::IPV4_MATCHER  =~ machine_name.to_s
    log(:debug, "[connect_to_machine_by_name] Connecting to machine by IP ADDRESS: #{ machine_name.to_s }")
    machine = Qcmd::Machine.new(machine_name, machine_name.to_s, 53000)
  end

  if machine.nil?
    if machine_name.nil? || machine_name.to_s.empty?
      log(:warning, 'You must include a machine name to connect.')
    else
      log(:warning, 'Sorry, that machine could not be found')
    end

    disconnected_machine_warning
  else
    print "Connecting to machine: #{machine_name}"
    connect_machine machine
  end
end

#connect_to_workspace_by_index(workspace_idx, passcode) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/qcmd/cli.rb', line 230

def connect_to_workspace_by_index workspace_idx, passcode
  if Qcmd.context.machine_connected?
    if workspace = Qcmd.context.machine.workspaces[workspace_idx]
      connect_to_workspace_by_name workspace.name, passcode
    else
      print "That workspace isn't on the list."
    end
  else
    log(:warning, %[You can't connect to a workspace until you've connected to a machine. ])
    disconnected_machine_warning
  end
end

#connect_to_workspace_by_name(workspace_name, passcode) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/qcmd/cli.rb', line 243

def connect_to_workspace_by_name workspace_name, passcode
  if Qcmd.context.machine_connected?
    if workspace = Qcmd.context.machine.find_workspace(workspace_name)
      workspace.passcode = passcode
      print "Connecting to workspace: #{workspace_name}"
      use_workspace workspace
    else
      log(:warning, "That workspace doesn't seem to exist, try one of the following:")
      Qcmd.context.machine.workspaces.each do |ws|
        log(:warning, %[  "#{ ws.name }"])
      end
    end
  else
    log(:warning, %[You can't connect to a workspace until you've connected to a machine. ])
    disconnected_machine_warning
  end
end

#disconnected_machine_warningObject



181
182
183
184
185
186
187
188
189
190
# File 'lib/qcmd/cli.rb', line 181

def disconnected_machine_warning
  if Qcmd::Network.names.size > 0
    print "Try one of the following:"
    Qcmd::Network.names.each do |name|
      print %[  #{ name }]
    end
  else
    print "There are no QLab machines on this network :("
  end
end

#expand_alias(key, expression) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/qcmd/cli.rb', line 113

def expand_alias key, expression
  Qcmd.debug "[CLI expand_alias] using alias of #{ key } with #{ expression.inspect }"

  new_command = aliases[key]

  # observe alias arity
  argument_placeholders = new_command.scan(alias_arg_matcher).uniq.map {|placeholder|
    placeholder[0].sub(/$\$/, '').to_i
  }

  if argument_placeholders.size > 0
    arguments_expected = argument_placeholders.max

    # because expression is alias + arguments, the expression's size should
    # be at least arguments_expected + 1
    if expression.size <= arguments_expected
      print "This custom command expects at least #{ arguments_expected } arguments."
      return
    end
  end

  new_command = Parser.parse(new_command)
  new_command = replace_args(new_command, expression)

  new_command
end

#get_promptObject



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/qcmd/cli.rb', line 140

def get_prompt
  clock = Time.now.strftime "%H:%M"
  prefix = []

  if Qcmd.context.machine_connected?
    prefix << "[#{ Qcmd.context.machine.name }]"
  end

  if Qcmd.context.workspace_connected?
    prefix << "[#{ Qcmd.context.workspace.name }]"
  end

  if Qcmd.context.cue_connected?
    prefix << "[#{ Qcmd.context.cue.number } #{ Qcmd.context.cue.name }]"
  end

  ["#{clock} #{prefix.join(' ')}", "> "]
end

#handle_failed_workspace_command(command) ⇒ Object



647
648
649
650
651
652
# File 'lib/qcmd/cli.rb', line 647

def handle_failed_workspace_command command
  command = command.join ' '
  print_wrapped(%[The command, "#{ command }" can't be processed yet. you must
                  first connect to a machine and a workspace
                  before issuing other commands.])
end

#handle_input(args) ⇒ Object

the actual command line interface interactor



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
# File 'lib/qcmd/cli.rb', line 330

def handle_input args
  if args.all? {|a| a.is_a?(Array)}
    # commands all the way down, just get out of the way
    args.each {|arg|
      Qcmd.debug "calling recursive handle_input on #{ arg.inspect }"
      handle_input(arg)
    }
    return
  else
    command = args[0].to_s
  end

  Qcmd.debug "[CLI handle_input] command: #{ command }; args: #{ args.inspect }"

  # this is where qcmd decides how to handle user input

  case command
  when 'exit', 'quit', 'q'
    print 'exiting...'
    exit 0

  when 'connect'
    Qcmd.debug "[CLI handle_input] connect command received args: #{ args.inspect } :: #{ args.map {|a| a.class.to_s}.inspect}"

    machine_ident = args[1]

    if machine_ident.is_a?(Fixnum)
      # machine "index" will be given with a 1-indexed value instead of the
      # stored 0-indexed value.
      connect_to_machine_by_index machine_ident - 1
    else
      connect_to_machine_by_name machine_ident
    end

    if Qcmd.context.machine_connected?
      load_workspaces

      if !connect_default_workspace
        Handler.print_workspace_list
      end
    end

  when 'disconnect'
    disconnect_what = args[1]

    if disconnect_what == 'workspace'
      Qcmd.context.disconnect_cue
      Qcmd.context.disconnect_workspace

      Handler.print_workspace_list
    elsif disconnect_what == 'cue'
      Qcmd.context.disconnect_cue
    else
      reset
      Qcmd::Network.browse_and_display
    end

  when '..'
    if Qcmd.context.cue_connected?
      Qcmd.context.disconnect_cue
    elsif Qcmd.context.workspace_connected?
      Qcmd.context.disconnect_workspace
    else
      reset
    end

  when 'use'
    Qcmd.debug "[CLI handle_input] use command received args: #{ args.inspect }"

    workspace_name = args[1]
    passcode       = args[2]

    Qcmd.debug "[CLI handle_input] using workspace: #{ workspace_name.inspect }"

    if workspace_name
      if workspace_name.is_a?(Fixnum)
        # decrement given idx
        connect_to_workspace_by_index workspace_name - 1, passcode
      else
        connect_to_workspace_by_name workspace_name, passcode
      end
    else
      print "No workspace name given. The following workspaces are available:"
      Handler.print_workspace_list
    end

  when 'workspaces'
    if !Qcmd.context.machine_connected?
      disconnected_machine_warning
    else
      machine.workspaces = Qcmd::Action.evaluate(args).map {|ws|
        QLab::Workspace.new(ws)
      }
      Handler.print_workspace_list
    end

  when 'workspace'
    workspace_command = args[1]

    if !Qcmd.context.workspace_connected?
      handle_failed_workspace_command args
      return
    end

    if workspace_command.nil?
      print_wrapped("no workspace command given. available workspace commands
                     are: #{Qcmd::InputCompleter::ReservedWorkspaceWords.join(', ')}")
    else
      reply = send_workspace_command(workspace_command, *args)
      handle_simple_reply reply
    end

  when 'help'
    Qcmd::Commands::Help.print_all_commands

  when 'cues'
    if !Qcmd.context.workspace_connected?
      handle_failed_workspace_command args
      return
    end

    # reload cues
    load_cues

    Qcmd.context.workspace.cue_lists.each do |cue_list|
      print
      print centered_text(" Cues: #{ cue_list.name } ", '-')
      printable_cues = []

      add_cues_to_list cue_list, printable_cues, 0

      table ['Number', 'Id', 'Name', 'Type'], printable_cues

      print
    end

  when /^(cue|cue_id)$/
    # id_field = $1

    if !Qcmd.context.workspace_connected?
      handle_failed_workspace_command args
      return
    end

    if args.size < 3
      print "Cue commands should be in the form:"
      print
      print "  > cue NUMBER COMMAND [ARGUMENTS]"
      print
      print "or"
      print
      print "  > cue_id ID COMMAND [ARGUMENTS]"
      print
      print_wrapped("available cue commands are: #{Qcmd::Commands::CUE.join(', ')}")
      print
      return
    end

    cue_action = Qcmd::CueAction.new(args)

    reply = cue_action.evaluate
    handle_simple_reply reply

    fixate_on_cue(cue_action)

  when 'aliases'
    print centered_text(" Available Custom Commands ", '-')
    print

    aliases.each do |(key, val)|
      print key
      print '    ' + word_wrap(val, :indent => '    ', :preserve_whitespace => true).join("\n")
      print
    end

  when 'alias'
    new_alias = add_alias(args[1].to_s, args[2])
    print %[Added alias for "#{ args[1] }": #{ new_alias }]

  when 'new'
    # create new cue

    if !(args.size == 2 && QLab::Cue::TYPES.include?(args.last.to_s))
      log(:warning, "That cue type can't be created, try one of the following:")
      log(:warning, joined_wrapped(QLab::Cue::TYPES.join(", ")))
    else
      reply = send_workspace_command(command, *args)
      handle_simple_reply reply
    end

  when 'select'
    if args.size == 2
      reply = send_workspace_command "#{ args[0] }/#{ args[1] }"

      if reply.respond_to?(:status) && reply.status == 'ok'
        # cue exists, get name and fixate
        cue_action = Qcmd::CueAction.new([:cue, args[1], :name])
        reply = cue_action.evaluate
        if reply.is_a?(QLab::Reply)
          # something went wrong
          handle_simple_reply reply
        else
          print "Selected #{args[1]} - #{reply}"
          fixate_on_cue(cue_action)
        end
      end
    else
      log(:warning, "The select command should be in the form `select CUE_NUMBER`.")
    end

  # local commands
  when 'sleep'
    if args.size != 2
      log(:warning, "The sleep command expects one argument")
    elsif !(args[1].is_a?(Fixnum) || args[1].is_a?(Float))
      log(:warning, "The sleep command expects a number")
    else
      sleep args[1].to_f
    end

  when 'log-silent'
    @previous_log_level = Qcmd.log_level
    Qcmd.log_level = :none

  when 'log-noisy'
    Qcmd.log_level = @previous_log_level || :info

  when 'log-debug'
    Qcmd.log_level = :debug
    print "set log level to :debug"

  when 'log-info'
    Qcmd.log_level = :info
    print "set log level to :info"

  when 'echo'
    if args[1].is_a?(Array)
      print Action.evaluate(args[1])
    else
      print args[1]
    end

  else
    if aliases[command]
      Qcmd.debug "[CLI handle_input] using alias #{ command }"

      new_expression = expand_alias(command, args)

      # alias expansion failed, go back to CLI
      return if new_expression.nil?

      # unpack nested command. e.g., [[:cue, 1, :name]] -> [:cue, 1, :name]
      if new_expression.size == 1 && new_expression[0].is_a?(Array)
        while new_expression.size == 1 && new_expression[0].is_a?(Array)
          new_expression = new_expression[0]
        end
      end

      Qcmd.debug "[CLI handle_input] expanded to: #{ new_expression.inspect }"

      # recurse!
      if new_expression.all? {|exp| exp.is_a?(Array)}
        new_expression.each {|nested_expression|
          handle_input nested_expression
        }
      else
        handle_input(new_expression)
      end

    elsif Qcmd.context.cue_connected? && Qcmd::InputCompleter::ReservedCueWords.include?(command)
      # prepend the given command with a cue address
      if Qcmd.context.cue.number.nil? || Qcmd.context.cue.number.size == 0
        command_args = [:cue_id, Qcmd.context.cue.id, command]
      else
        command_args = [:cue, Qcmd.context.cue.number, command]
      end

      # add the rest of the given args
      Qcmd.debug "adding #{args[1..-1].inspect} to #{ command_args.inspect }"
      command_args.push(*args[1..-1])

      Qcmd.debug "creating cue action with #{command_args.inspect}"
      cue_action = Qcmd::CueAction.new(command_args)

      reply = cue_action.evaluate
      handle_simple_reply reply

    elsif Qcmd.context.workspace_connected? && Qcmd::InputCompleter::ReservedWorkspaceWords.include?(command)
      reply = send_workspace_command(command, *args)
      handle_simple_reply reply

    else
      # failure modes?
      if %r[/] =~ command
        # might be legit OSC command, try sending
        reply = Qcmd::Action.evaluate(args)
        handle_simple_reply reply
      else
        if Qcmd.context.cue_connected?
          # cue is connected, but command isn't a valid cue command
          print_wrapped("Unrecognized command: '#{ command }'. Try one of these cue commands: #{ Qcmd::InputCompleter::ReservedCueWords.join(', ') }")
          print 'or disconnect from the cue with ..'
        elsif Qcmd.context.workspace_connected?
          # workspace is connected, but command isn't a valid workspace command
          print_wrapped("Unrecognized command: '#{ command }'. Try one of these workspace commands: #{ Qcmd::InputCompleter::ReservedWorkspaceWords.join(', ') }")
        elsif Qcmd.context.machine_connected?
          # send a command directly to a machine
          reply = Qcmd::Action.evaluate(args)
          handle_simple_reply reply
        else
          print 'you must connect to a machine before sending commands'
        end
      end
    end
  end
end

#machineObject



51
52
53
# File 'lib/qcmd/cli.rb', line 51

def machine
  Qcmd.context.machine
end

#replace_args(alias_expression, original_expression) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/qcmd/cli.rb', line 75

def replace_args alias_expression, original_expression
  Qcmd.debug "[CLI replace_args] populating #{ alias_expression.inspect } with #{ original_expression.inspect }"

  alias_expression.map do |arg|
    if arg.is_a?(Array)
      replace_args(arg, original_expression)
    elsif (arg.is_a?(Symbol) || arg.is_a?(String)) && alias_arg_matcher =~ arg.to_s
      while alias_arg_matcher =~ arg.to_s
        arg_idx = $1.to_i
        arg_val = original_expression[arg_idx]

        Qcmd.debug "[CLI replace_args] found $#{ arg_idx }, replacing with #{ arg_val.inspect }"

        if arg == :"$#{ arg_idx }"
          # pure symbol replace
          #   alias: [:cue, :$1, :name]
          #   input: [:cname, 25]
          #
          #   result:  :$1 -> 25
          arg = arg_val
        else
          # arg replacement inside string
          #   alias: [:cue, :$1, :name, "hello $2"]
          #   input: [:cname, 25, 26]
          #
          #   result:  :$1 -> 25
          #   result:  "hello $2" -> "hello 26"
          arg = arg.to_s.sub("$#{ arg_idx }", arg_val.to_s)
        end
      end

      arg
    else
      arg
    end
  end
end

#resetObject



55
56
57
# File 'lib/qcmd/cli.rb', line 55

def reset
  Qcmd.context.reset
end

#split_and_handle(cli_input) ⇒ Object



319
320
321
322
323
324
325
326
327
# File 'lib/qcmd/cli.rb', line 319

def split_and_handle cli_input
  if /;/ =~ cli_input
    cli_input.split(';').each do |sub_input|
      handle_input Qcmd::Parser.parse(sub_input.strip)
    end
  else
    handle_input Qcmd::Parser.parse(cli_input)
  end
end

#startObject



294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/qcmd/cli.rb', line 294

def start
  loop do
    # blocks the whole Ruby VM
    prefix, char = get_prompt

    Qcmd.print prefix
    cli_input = Readline.readline(char, true)

    if cli_input.nil? || cli_input.size == 0
      Qcmd.debug "[CLI start] got: #{ cli_input.inspect }"
      next
    end

    # save all commands to log
    Qcmd::History.push(cli_input)

    begin
      split_and_handle(cli_input)
    rescue => ex
      print "Command parser couldn't handle the last command: #{ ex.message }"
      print ex.backtrace
    end
  end
end

#use_workspace(workspace) ⇒ Object



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/qcmd/cli.rb', line 261

def use_workspace workspace
  Qcmd.debug %[[CLI use_workspace] connecting to workspace: "#{workspace.name}"]

  # set workspace in context. Will unset later if there's a problem.
  Qcmd.context.workspace = workspace

  # send connect message to QLab to make sure subsequent messages target it
  if workspace.passcode?
    ws_action_string = "workspace/#{workspace.id}/connect %04i" % workspace.passcode
  else
    ws_action_string = "workspace/#{workspace.id}/connect"
  end

  reply = Qcmd::Action.evaluate(ws_action_string)

  if reply == 'badpass'
    log(:error, 'Failed to connect to workspace, bad passcode or no passcode given.')
    Qcmd.context.disconnect_workspace
  elsif reply == 'ok'
    print %[Connected to "#{Qcmd.context.workspace.name}"]
    Qcmd.context.workspace_connected = true
  end

  # if it worked, load cues automatically
  if Qcmd.context.workspace_connected?
    load_cues

    if Qcmd.context.workspace.cue_lists
      print "Loaded #{pluralize Qcmd.context.workspace.cues.size, 'cue'}"
    end
  end
end