qcmd 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -13,7 +13,12 @@ IT.**
13
13
 
14
14
  ## Installation
15
15
 
16
- Install this gem to your machine by running the following command:
16
+ Before installing qcmd, you'll have to install the [Command Line Tools for
17
+ Xcode](https://developer.apple.com/downloads). They're free, but you'll need an
18
+ Apple ID to download them.
19
+
20
+ Once you've done that, you can install qcmd to your machine by running the
21
+ following command:
17
22
 
18
23
  $ sudo gem install qcmd
19
24
 
@@ -33,7 +38,7 @@ send commands to cues and the workspace.
33
38
  wondering what you can do from the console.
34
39
 
35
40
  Run `qcmd` with the -v option to get full debugging output. Use the main
36
- project repository (https://github.com/abachman/qcmd) to report any issues.
41
+ project repository (https://github.com/Figure53/qcmd) to report any issues.
37
42
 
38
43
  An example session might look like this:
39
44
 
data/TODO.md CHANGED
@@ -0,0 +1,31 @@
1
+ ### copyable fields
2
+
3
+ * sliderLevels
4
+ * fade shape
5
+ * action
6
+ * preWait
7
+ * postWait
8
+ * AU status?
9
+ * geometry
10
+ * full matrix
11
+
12
+
13
+ ### aliases and composable actions
14
+
15
+ `alias n (cue $1 name $2)` creates a new command, "n" that can be called just
16
+ like any built in command. `n 20 "Basic Intro"` expands into `cue 20 name "Basic Intro"`.
17
+
18
+ All actions should have a return value, then we can nest commands to create new
19
+ actions:
20
+
21
+ `alias copy-name (cue $2 name (cue $1 name))`
22
+
23
+ The **$** argument operator can be included in double quoted strings to act as
24
+ a simple templating tool. For example: `alias act-scene (cue $1 name "Act $2: Scene $3")`
25
+ could be used to give cues a specifc name with some plugged-in values. Now using
26
+ `act-scene 20 1 2` would give cue number 20 the name "Act 1: Scene 2".
27
+
28
+ Because custom actions expand into qcmd's normal actions, wildcards should work
29
+ fine. `act-scene 20.* 1 2` would change the name of every cue whose number
30
+ starts with "20." to "Act 1: Scene 2".
31
+
data/bin/qcmd CHANGED
@@ -10,7 +10,6 @@ opts = Trollop::options do
10
10
  opt :verbose, 'Use verbose mode', :default => false
11
11
  opt :debug, "Show full debug output, don't make changes to workspaces", :default => false
12
12
  opt :machine, "Automatically try to connect to the machine with the given name", :type => :string
13
- opt :machine_passcode, "Use the given machine passcode", :type => :integer
14
13
  opt :workspace, "Automatically try to connect to the workspace with the given name", :type => :string
15
14
  opt :workspace_passcode, "Use the given workspace passcode", :type => :integer
16
15
  opt :command, "Execute a single command and exit", :type => :string
@@ -27,6 +26,8 @@ end
27
26
 
28
27
  # browse local network and check for qlab + qlab workspaces
29
28
 
29
+ Qcmd::History.load
30
+
30
31
  if !opts[:machine_given]
31
32
  Qcmd.ascii_qlab
32
33
  Qcmd.print
@@ -1,17 +1,21 @@
1
+ # communicate!
1
2
  require 'socket'
2
3
  require 'osc-ruby'
3
4
 
4
- require 'qcmd/version'
5
+ # data from QLab
6
+ require 'json'
5
7
 
8
+ require 'qcmd/version'
6
9
  require 'qcmd/plaintext'
7
10
  require 'qcmd/commands'
8
11
  require 'qcmd/input_completer'
9
-
10
12
  require 'qcmd/core_ext/array'
11
13
  require 'qcmd/core_ext/osc/message'
12
- require 'qcmd/core_ext/osc/stopping_server'
14
+ require 'qcmd/core_ext/osc/tcp_client'
13
15
 
14
16
  module Qcmd
17
+ autoload :Configuration, 'qcmd/configuration'
18
+ autoload :History, 'qcmd/history'
15
19
  autoload :Handler, 'qcmd/handler'
16
20
  autoload :Server, 'qcmd/server'
17
21
  autoload :Context, 'qcmd/context'
@@ -21,6 +25,10 @@ module Qcmd
21
25
  autoload :Network, 'qcmd/network'
22
26
  autoload :QLab, 'qcmd/qlab'
23
27
  autoload :VERSION, 'qcmd/version'
28
+ autoload :Action, 'qcmd/action'
29
+ autoload :Aliases, 'qcmd/aliases'
30
+
31
+ # on launch
24
32
 
25
33
  class << self
26
34
  include Qcmd::Plaintext
@@ -61,6 +69,10 @@ module Qcmd
61
69
  end
62
70
 
63
71
  def debug message
72
+ # always write to log
73
+
74
+ Qcmd::Configuration.log.puts "[%s] %s" % [Time.now.strftime('%T'), message]
75
+
64
76
  log(message) if log_level == :debug
65
77
  end
66
78
 
@@ -0,0 +1,160 @@
1
+ module Qcmd
2
+ class Action
3
+ attr_reader :code
4
+
5
+ # initialize and evaluate in one shot
6
+ def self.evaluate action_input
7
+ is_cue_action = false
8
+
9
+ if action_input.is_a?(String)
10
+ is_cue_action = %w(cue cue_id).include?(action_input.split.first)
11
+ else
12
+ is_cue_action = ['cue', 'cue_id', :cue, :cue_id].include?(action_input.first)
13
+ end
14
+
15
+ if is_cue_action
16
+ CueAction.new(action_input).evaluate
17
+ else
18
+ Action.new(action_input).evaluate
19
+ end
20
+ end
21
+
22
+ def initialize(expression)
23
+ if expression.is_a?(String)
24
+ expression = Qcmd::Parser.parse(expression)
25
+ end
26
+
27
+ @code = parse(expression)
28
+ end
29
+
30
+ def evaluate
31
+ if code.size == 0
32
+ nil
33
+ else
34
+ @code = code.map do |token|
35
+ if token.is_a?(Action)
36
+ Qcmd.debug "[Action evaluate] evaluating nested action: #{ token.code.inspect }"
37
+ token.evaluate
38
+ else
39
+ token
40
+ end
41
+ end
42
+
43
+ Qcmd.debug "[Action evaluate] evaluating code: #{ code.inspect }"
44
+
45
+ send_message
46
+ end
47
+ end
48
+
49
+ def parse(expression)
50
+ # unwrap nested arrays
51
+ if expression.size == 1 && expression[0].is_a?(Array)
52
+ expression = expression[0]
53
+ end
54
+
55
+ expression.map do |token|
56
+ if token.is_a?(Array)
57
+ if [:cue, :cue_id].include?(token.first)
58
+ Qcmd.debug "nested cue action detected in #{ expression.inspect }"
59
+ CueAction.new(token)
60
+ else
61
+ Action.new(token)
62
+ end
63
+ else
64
+ token
65
+ end
66
+ end.tap {|exp|
67
+ Qcmd.debug "[Action parse] returning: #{ exp.inspect }"
68
+ }
69
+ end
70
+
71
+ # the default command builder
72
+ def osc_message
73
+ OSC::Message.new osc_address.to_s, *osc_arguments
74
+ end
75
+
76
+ def osc_address
77
+ # prefix w/ slash if necessary
78
+ if %r[^/] !~ code[0].to_s
79
+ "/#{ code[0] }"
80
+ else
81
+ code[0]
82
+ end
83
+ end
84
+
85
+ def osc_address=(value)
86
+ code[0] = value
87
+ end
88
+
89
+ def osc_arguments
90
+ code[1..-1]
91
+ end
92
+
93
+ # the raw command
94
+ def command
95
+ code[0]
96
+ end
97
+
98
+ private
99
+
100
+ def send_message
101
+ responses = []
102
+
103
+ Qcmd.context.qlab.send(osc_message) do |response|
104
+ # puts "response to: #{ osc_message.inspect }"
105
+ # puts response.inspect
106
+
107
+ responses << QLab::Reply.new(response)
108
+ end
109
+
110
+ if responses.size == 1
111
+ q_reply = responses[0]
112
+ Qcmd.debug "[Action send_message] got one response: #{q_reply.inspect}"
113
+
114
+ if q_reply.has_data?
115
+ q_reply.data
116
+ else
117
+ q_reply
118
+ end
119
+ else
120
+ responses
121
+ end
122
+ end
123
+ end
124
+
125
+ class CueAction < Action
126
+ # cue commands work differently
127
+ def command
128
+ code[2]
129
+ end
130
+
131
+ def osc_address
132
+ "/#{ code[0] }/#{ code[1] }/#{ code[2] }"
133
+ end
134
+
135
+ def osc_arguments
136
+ args = code[3..-1]
137
+ if args.nil?
138
+ nil
139
+ else
140
+ args.map {|arg|
141
+ if arg.is_a?(Symbol)
142
+ arg.to_s
143
+ else
144
+ arg
145
+ end
146
+ }
147
+ end
148
+ end
149
+
150
+ # cue specific fields
151
+
152
+ def id_field
153
+ code[0]
154
+ end
155
+
156
+ def identifier
157
+ code[1]
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,25 @@
1
+ module Qcmd
2
+ class Aliases
3
+ def self.defaults
4
+ @defaults ||= {
5
+ 'n' => 'cue $1 name $2',
6
+ # zero-out cue_number
7
+ 'zero-out' => (1..48).map {|n| "(cue $1 sliderLevel #{n} 0)"}.join(' '),
8
+ # copy-sliders from_cue_number to_cue_number
9
+ 'copy-sliders' => (1..48).map {|n| "(cue $2 sliderLevel #{n} (cue $1 sliderLevel #{n} 0))"}.join(' ')
10
+ }.merge(copy_cue_actions)
11
+ end
12
+
13
+ def self.copy_cue_actions
14
+ Hash[
15
+ %w(name notes fileTarget cueTargetNumber cueTargetId preWait duration
16
+ postWait continueMode flagged armed colorName).map do |field|
17
+ [
18
+ "copy-#{ field }",
19
+ "(cue $2 #{ field } (cue $1 #{ field }))"
20
+ ]
21
+ end
22
+ ]
23
+ end
24
+ end
25
+ end
@@ -1,124 +1,304 @@
1
- require 'qcmd/server'
2
-
3
1
  require 'readline'
4
-
5
2
  require 'osc-ruby'
6
3
 
7
4
  module Qcmd
8
5
  class CLI
9
6
  include Qcmd::Plaintext
10
7
 
11
- attr_accessor :server, :prompt
8
+ attr_accessor :prompt
12
9
 
13
10
  def self.launch options={}
14
11
  new options
15
12
  end
16
13
 
17
14
  def initialize options={}
18
- Qcmd.debug "(launching with options: #{options.inspect})"
19
- # start local listening port
20
- Qcmd.context = Qcmd::Context.new
15
+ Qcmd.debug "[CLI initialize] launching with options: #{options.inspect}"
21
16
 
22
- self.prompt = '> '
17
+ Qcmd.context = Qcmd::Context.new
23
18
 
24
19
  if options[:machine_given]
25
- Qcmd.debug "(autoconnecting to #{ options[:machine] })"
20
+ Qcmd.debug "[CLI initialize] autoconnecting to machine #{ options[:machine] }"
26
21
 
27
22
  Qcmd.while_quiet do
28
- connect_to_machine_by_name options[:machine], options[:machine_passcode]
23
+ connect_to_machine_by_name(options[:machine])
29
24
  end
30
25
 
31
26
  if options[:workspace_given]
27
+ Qcmd.debug "[CLI initialize] autoconnecting to workspace #{ options[:machine] }"
28
+
32
29
  Qcmd.while_quiet do
33
- connect_to_workspace_by_name options[:workspace], options[:workspace_passcode]
30
+ connect_to_workspace_by_name(options[:workspace], options[:workspace_passcode])
34
31
  end
35
32
 
36
33
  if options[:command_given]
37
- handle_message options[:command]
34
+ handle_input options[:command]
38
35
  print %[sent command "#{ options[:command] }"]
39
36
  exit 0
40
37
  end
38
+ elsif Qcmd.context.machine.workspaces.size == 1 &&
39
+ !Qcmd.context.machine.workspaces.first.passcode? &&
40
+ !Qcmd.context.workspace_connected?
41
+ connect_to_workspace_by_index(0, nil)
41
42
  end
42
43
  end
43
44
 
45
+ # add aliases to input completer
46
+ InputCompleter.add_commands aliases.keys
47
+
44
48
  start
45
49
  end
46
50
 
47
- def connect machine, passcode
51
+ def machine
52
+ Qcmd.context.machine
53
+ end
54
+
55
+ def reset
56
+ Qcmd.context.reset
57
+ end
58
+
59
+ def aliases
60
+ @aliases ||= Qcmd::Aliases.defaults.merge(Qcmd::Configuration.config['aliases'])
61
+ end
62
+
63
+ def alias_arg_matcher
64
+ /\$(\d+)/
65
+ end
66
+
67
+ def add_alias name, expression
68
+ aliases[name] = Parser.generate(expression)
69
+ InputCompleter.add_command name
70
+ Qcmd::Configuration.update('aliases', aliases)
71
+
72
+ aliases[name]
73
+ end
74
+
75
+ def replace_args alias_expression, original_expression
76
+ Qcmd.debug "[CLI replace_args] populating #{ alias_expression.inspect } with #{ original_expression.inspect }"
77
+
78
+ alias_expression.map do |arg|
79
+ if arg.is_a?(Array)
80
+ replace_args(arg, original_expression)
81
+ elsif (arg.is_a?(Symbol) || arg.is_a?(String)) && alias_arg_matcher =~ arg.to_s
82
+ while alias_arg_matcher =~ arg.to_s
83
+ arg_idx = $1.to_i
84
+ arg_val = original_expression[arg_idx]
85
+
86
+ Qcmd.debug "[CLI replace_args] found $#{ arg_idx }, replacing with #{ arg_val.inspect }"
87
+
88
+ arg = arg.to_s.sub("$#{ arg_idx }", arg_val.to_s)
89
+ end
90
+
91
+ arg
92
+ else
93
+ arg
94
+ end
95
+ end
96
+ end
97
+
98
+ def expand_alias key, expression
99
+ Qcmd.debug "[CLI expand_alias] using alias of #{ key } with #{ expression.inspect }"
100
+
101
+ new_command = aliases[key]
102
+
103
+ # observe alias arity
104
+ argument_placeholders = new_command.scan(alias_arg_matcher).uniq.map {|placeholder|
105
+ placeholder[0].sub(/$\$/, '').to_i
106
+ }
107
+
108
+ if argument_placeholders.size > 0
109
+ arguments_expected = argument_placeholders.max
110
+
111
+ # because expression is alias + arguments, the expression's size should
112
+ # be at least arguments_expected + 1
113
+ if expression.size <= arguments_expected
114
+ print "This custom command expects at least #{ arguments_expected } arguments."
115
+ return
116
+ end
117
+ end
118
+
119
+ new_command = Parser.parse(new_command)
120
+ new_command = replace_args(new_command, expression)
121
+
122
+ new_command
123
+ end
124
+
125
+ def get_prompt
126
+ clock = Time.now.strftime "%H:%M"
127
+ prefix = []
128
+
129
+ if Qcmd.context.machine_connected?
130
+ prefix << "[#{ Qcmd.context.machine.name }]"
131
+ end
132
+
133
+ if Qcmd.context.workspace_connected?
134
+ prefix << "[#{ Qcmd.context.workspace.name }]"
135
+ end
136
+
137
+ if Qcmd.context.cue_connected?
138
+ prefix << "[#{ Qcmd.context.cue.number } #{ Qcmd.context.cue.name }]"
139
+ end
140
+
141
+ ["#{clock} #{prefix.join(' ')}", "> "]
142
+ end
143
+
144
+ def connect machine
48
145
  if machine.nil?
49
146
  print "A valid machine is needed to connect!"
50
147
  return
51
148
  end
52
149
 
150
+ reset
151
+
53
152
  Qcmd.context.machine = machine
54
- Qcmd.context.workspace = nil
55
153
 
56
- if server.nil?
57
- # set client connection and start listening port
58
- self.server = Qcmd::Server.new :receive => 53001
59
- else
60
- # change client connection
61
- server.connect_to_client
154
+ # in case this is a reconnection
155
+ Qcmd.context.connect_to_qlab
156
+
157
+ # tell QLab to always reply to messages
158
+ response = Qcmd::Action.evaluate('/alwaysReply 1')
159
+ if response.nil? || response.empty?
160
+ print %[Failed to connect to QLab machine "#{ machine.name }"]
161
+ elsif response.status == 'ok'
162
+ print %[Connected to machine "#{ machine.name }"]
62
163
  end
63
- server.run
64
164
 
65
- server.load_workspaces
165
+ machine.workspaces = Qcmd::Action.evaluate('workspaces').map {|ws| QLab::Workspace.new(ws)}
166
+
167
+ if Qcmd.context.machine.workspaces.size == 1 && !Qcmd.context.machine.workspaces.first.passcode?
168
+ connect_to_workspace_by_index(0, nil)
169
+ else
170
+ Handler.print_workspace_list
171
+ end
172
+ end
66
173
 
67
- self.prompt = "#{ machine.name }> "
174
+ def disconnected_machine_warning
175
+ if Qcmd::Network.names.size > 0
176
+ print "Try one of the following:"
177
+ Qcmd::Network.names.each do |name|
178
+ print %[ #{ name }]
179
+ end
180
+ else
181
+ print "There are no QLab machines on this network :("
182
+ end
68
183
  end
69
184
 
70
- def connect_to_machine_by_name machine_name, passcode
185
+ def connect_to_machine_by_name machine_name
71
186
  if machine = Qcmd::Network.find(machine_name)
72
- print "connecting to machine: #{machine_name}"
73
- connect machine, passcode
187
+ print "Connecting to machine: #{machine_name}"
188
+ connect machine
189
+ else
190
+ print 'Sorry, that machine could not be found'
191
+ end
192
+ end
193
+
194
+ def connect_to_machine_by_index machine_idx
195
+ if machine = Qcmd::Network.find_by_index(machine_idx)
196
+ print "Connecting to machine: #{machine.name}"
197
+ connect machine
74
198
  else
75
- print 'sorry, that machine could not be found'
199
+ print 'Sorry, that machine could not be found'
200
+ end
201
+ end
202
+
203
+ def connect_to_workspace_by_index workspace_idx, passcode
204
+ if Qcmd.context.machine_connected?
205
+ if workspace = Qcmd.context.machine.workspaces[workspace_idx]
206
+ connect_to_workspace_by_name workspace.name, passcode
207
+ else
208
+ print "That workspace isn't on the list."
209
+ end
210
+ else
211
+ print %[You can't connect to a workspace until you've connected to a machine. ]
212
+ disconnected_machine_warning
76
213
  end
77
214
  end
78
215
 
79
216
  def connect_to_workspace_by_name workspace_name, passcode
80
- if workspace = Qcmd.context.machine.find_workspace(workspace_name)
81
- workspace.passcode = passcode
82
- print "connecting to workspace: #{workspace_name}"
83
- use_workspace workspace
217
+ if Qcmd.context.machine_connected?
218
+ if workspace = Qcmd.context.machine.find_workspace(workspace_name)
219
+ workspace.passcode = passcode
220
+ print "Connecting to workspace: #{workspace_name}"
221
+
222
+ use_workspace workspace
223
+ else
224
+ print "That workspace doesn't seem to exist, try one of the following:"
225
+ Qcmd.context.machine.workspaces.each do |ws|
226
+ print %[ "#{ ws.name }"]
227
+ end
228
+ end
229
+ else
230
+ print %[You can't connect to a workspace until you've connected to a machine. ]
231
+ disconnected_machine_warning
84
232
  end
85
233
  end
86
234
 
87
235
  def use_workspace workspace
88
- Qcmd.debug %[(connecting to workspace: "#{workspace.name}")]
236
+ Qcmd.debug %[[CLI use_workspace] connecting to workspace: "#{workspace.name}"]
237
+
89
238
  # set workspace in context. Will unset later if there's a problem.
90
239
  Qcmd.context.workspace = workspace
91
240
 
92
- server.connect_to_workspace workspace
93
- if Qcmd.context.workspace_connected? && Qcmd.context.workspace.cues
94
- print "loaded #{pluralize Qcmd.context.workspace.cues.size, 'cue'}"
95
- self.prompt = "#{ Qcmd.context.machine.name }:#{ workspace.name }> "
241
+ # send connect message to QLab to make sure subsequent messages target it
242
+ if workspace.passcode?
243
+ ws_action_string = "workspace/#{workspace.id}/connect %04i" % workspace.passcode
244
+ else
245
+ ws_action_string = "workspace/#{workspace.id}/connect"
96
246
  end
97
- end
98
247
 
99
- def reset
100
- Qcmd.context.reset
101
- server.stop
102
- self.prompt = "> "
248
+ reply = Qcmd::Action.evaluate(ws_action_string)
249
+
250
+ if reply == 'badpass'
251
+ print 'Failed to connect to workspace, bad passcode or no passcode given.'
252
+ Qcmd.context.disconnect_workspace
253
+ elsif reply == 'ok'
254
+ print %[Connected to "#{Qcmd.context.workspace.name}"]
255
+ Qcmd.context.workspace_connected = true
256
+ end
257
+
258
+ # if it worked, load cues automatically
259
+ if Qcmd.context.workspace_connected?
260
+ load_cues
261
+
262
+ if Qcmd.context.workspace.cue_lists
263
+ print "Loaded #{pluralize Qcmd.context.workspace.cues.size, 'cue'}"
264
+ end
265
+ end
103
266
  end
104
267
 
105
268
  def start
106
269
  loop do
107
270
  # blocks the whole Ruby VM
108
- message = Readline.readline(prompt, true)
271
+ prefix, char = get_prompt
272
+
273
+ Qcmd.print prefix
274
+ cli_input = Readline.readline(char, true)
109
275
 
110
- if message.nil? || message.size == 0
111
- Qcmd.debug "(got: #{ message.inspect })"
276
+ if cli_input.nil? || cli_input.size == 0
277
+ Qcmd.debug "[CLI start] got: #{ cli_input.inspect }"
112
278
  next
113
279
  end
114
280
 
115
- handle_message(message)
281
+ # save all commands to log
282
+ Qcmd::History.push(cli_input)
283
+
284
+ begin
285
+ if /;/ =~ cli_input
286
+ cli_input.split(';').each do |sub_input|
287
+ handle_input Qcmd::Parser.parse(sub_input)
288
+ end
289
+ else
290
+ handle_input Qcmd::Parser.parse(cli_input)
291
+ end
292
+ rescue => ex
293
+ print "Command parser couldn't handle the last command: #{ ex.message }"
294
+ print ex.backtrace
295
+ end
116
296
  end
117
297
  end
118
298
 
119
- def handle_message message
120
- args = Qcmd::Parser.parse(message)
121
- command = args.shift
299
+ # the actual command line interface interactor
300
+ def handle_input args
301
+ command = args[0].to_s
122
302
 
123
303
  case command
124
304
  when 'exit', 'quit', 'q'
@@ -126,125 +306,340 @@ module Qcmd
126
306
  exit 0
127
307
 
128
308
  when 'connect'
129
- Qcmd.debug "(connect command received args: #{ args.inspect })"
309
+ Qcmd.debug "[CLI handle_input] connect command received args: #{ args.inspect } :: #{ args.map {|a| a.class.to_s}.inspect}"
130
310
 
131
- machine_name = args.shift
132
- passcode = args.shift
311
+ machine_ident = args[1]
133
312
 
134
- connect_to_machine_by_name machine_name, passcode
313
+ if machine_ident.is_a?(Fixnum)
314
+ # machine "index" will be given with a 1-indexed value instead of the
315
+ # stored 0-indexed value.
316
+ connect_to_machine_by_index machine_ident - 1
317
+ else
318
+ connect_to_machine_by_name machine_ident
319
+ end
135
320
 
136
321
  when 'disconnect'
137
- reset
138
- Qcmd::Network.browse_and_display
322
+ disconnect_what = args[1]
323
+
324
+ if disconnect_what == 'workspace'
325
+ Qcmd.context.disconnect_cue
326
+ Qcmd.context.disconnect_workspace
327
+
328
+ Handler.print_workspace_list
329
+ elsif disconnect_what == 'cue'
330
+ Qcmd.context.disconnect_cue
331
+ else
332
+ reset
333
+ Qcmd::Network.browse_and_display
334
+ end
335
+
336
+ when '..'
337
+ if Qcmd.context.cue_connected?
338
+ Qcmd.context.disconnect_cue
339
+ elsif Qcmd.context.workspace_connected?
340
+ Qcmd.context.disconnect_workspace
341
+ else
342
+ reset
343
+ end
139
344
 
140
345
  when 'use'
141
- Qcmd.debug "(use command received args: #{ args.inspect })"
346
+ Qcmd.debug "[CLI handle_input] use command received args: #{ args.inspect }"
142
347
 
143
- workspace_name = args.shift
144
- passcode = args.shift
348
+ workspace_name = args[1]
349
+ passcode = args[2]
145
350
 
146
- Qcmd.debug "(using workspace: #{ workspace_name.inspect })"
351
+ Qcmd.debug "[CLI handle_input] using workspace: #{ workspace_name.inspect }"
147
352
 
148
353
  if workspace_name
149
- connect_to_workspace_by_name workspace_name, passcode
354
+ if workspace_name.is_a?(Fixnum)
355
+ # decrement given idx
356
+ connect_to_workspace_by_index workspace_name - 1, passcode
357
+ else
358
+ connect_to_workspace_by_name workspace_name, passcode
359
+ end
150
360
  else
151
361
  print "No workspace name given. The following workspaces are available:"
152
- Qcmd.context.print_workspace_list
362
+ Handler.print_workspace_list
363
+ end
364
+
365
+ when 'workspaces'
366
+ if !Qcmd.context.machine_connected?
367
+ disconnected_machine_warning
368
+ else
369
+ machine.workspaces = Qcmd::Action.evaluate(args).map {|ws| QLab::Workspace.new(ws)}
370
+ Handler.print_workspace_list
371
+ end
372
+
373
+ when 'workspace'
374
+ workspace_command = args[1]
375
+
376
+ if !Qcmd.context.workspace_connected?
377
+ handle_failed_workspace_command cli_input
378
+ return
379
+ end
380
+
381
+ if workspace_command.nil?
382
+ print_wrapped("no workspace command given. available workspace commands
383
+ are: #{Qcmd::InputCompleter::ReservedWorkspaceWords.join(', ')}")
384
+ else
385
+ send_workspace_command(workspace_command, *args)
153
386
  end
154
387
 
155
388
  when 'help'
156
389
  help_command = args.shift
157
390
 
158
391
  if help_command.nil?
159
- Qcmd::Commands::Help.print_all_commands
160
392
  # print help according to current context
393
+ Qcmd::Commands::Help.print_all_commands
161
394
  else
162
395
  # print command specific help
163
396
  end
164
397
 
165
398
  when 'cues'
166
399
  if !Qcmd.context.workspace_connected?
167
- failed_workspace_command message
400
+ handle_failed_workspace_command cli_input
168
401
  return
169
402
  end
170
403
 
171
404
  # reload cues
172
- server.load_cues
405
+ load_cues
173
406
 
174
- print
175
- print centered_text(" Cues ", '-')
176
- table ['Number', 'Id', 'Name', 'Type'], Qcmd.context.workspace.cues.map {|cue|
177
- [cue.number, cue.id, cue.name, cue.type]
178
- }
179
- print
407
+ Qcmd.context.workspace.cue_lists.each do |cue_list|
408
+ print
409
+ print centered_text(" Cues: #{ cue_list.name } ", '-')
410
+ printable_cues = []
411
+
412
+ add_cues_to_list cue_list, printable_cues, 0
413
+
414
+ table ['Number', 'Id', 'Name', 'Type'], printable_cues
415
+
416
+ print
417
+ end
418
+
419
+ when /^(cue|cue_id)$/
420
+ # id_field = $1
180
421
 
181
- when 'cue', 'c'
182
422
  if !Qcmd.context.workspace_connected?
183
- failed_workspace_command message
423
+ handle_failed_workspace_command cli_input
184
424
  return
185
425
  end
186
426
 
187
- # pull off cue number
188
- cue_number = args.shift
189
- cue_action = args.shift
190
-
191
- if cue_number.nil?
192
- print "no cue command given. cue commands should be in the form:"
427
+ if args.size < 3
428
+ print "Cue commands should be in the form:"
193
429
  print
194
- print " > cue NUMBER COMMAND ARGUMENTS"
430
+ print " > cue NUMBER COMMAND [ARGUMENTS]"
195
431
  print
196
- print_wrapped("available cue commands are: #{Qcmd::Commands::ALL_CUE_COMMANDS.join(', ')}")
197
- elsif cue_action.nil?
198
- server.send_workspace_command("cue/#{ cue_number }")
199
- else
200
- server.send_cue_command(cue_number, cue_action, *args)
432
+ print "or"
433
+ print
434
+ print " > cue_id ID COMMAND [ARGUMENTS]"
435
+ print
436
+ print_wrapped("available cue commands are: #{Qcmd::Commands::CUE.join(', ')}")
437
+ print
438
+ return
201
439
  end
202
440
 
203
- when 'workspaces'
204
- if !Qcmd.context.machine_connected?
205
- print 'cannot load workspaces until you are connected to a machine'
206
- return
441
+ cue_action = Qcmd::CueAction.new(args)
442
+
443
+ reply = cue_action.evaluate
444
+
445
+ if reply.is_a?(QLab::Reply)
446
+ if !reply.status.nil?
447
+ print reply.status
448
+ end
449
+ else
450
+ render_data reply
207
451
  end
208
452
 
209
- server.load_workspaces
453
+ # fixate on cue
454
+ if Qcmd.context.workspace.has_cues?
455
+ _cue = Qcmd.context.workspace.cues.find {|cue|
456
+ case cue_action.id_field
457
+ when :cue
458
+ cue.number.to_s == cue_action.identifier.to_s
459
+ when :cue_id
460
+ cue.id.to_s == cue_action.identifier.to_s
461
+ end
462
+ }
210
463
 
211
- when 'workspace'
212
- workspace_command = args.shift
464
+ if _cue
465
+ Qcmd.context.cue = _cue
466
+ Qcmd.context.cue_connected = true
213
467
 
214
- if !Qcmd.context.workspace_connected?
215
- handle_failed_workspace_command message
216
- return
468
+ Qcmd.context.cue.sync
469
+ end
217
470
  end
218
471
 
219
- if workspace_command.nil?
220
- print_wrapped("no workspace command given. available workspace commands
221
- are: #{Qcmd::InputCompleter::ReservedWorkspaceWords.join(', ')}")
222
- else
223
- server.send_workspace_command(workspace_command, *args)
472
+ when 'aliases'
473
+ print centered_text(" Available Custom Commands ", '-')
474
+ print
475
+
476
+ aliases.each do |(key, val)|
477
+ print key
478
+ print ' ' + word_wrap(val, :indent => ' ', :preserve_whitespace => true).join("\n")
479
+ print
224
480
  end
225
481
 
482
+ when 'alias'
483
+ new_alias = add_alias(args[1].to_s, args[2])
484
+ print %[Added alias for "#{ args[1] }": #{ new_alias }]
485
+
226
486
  else
227
- if Qcmd.context.workspace_connected?
228
- if Qcmd::InputCompleter::ReservedWorkspaceWords.include?(command)
229
- server.send_workspace_command(command, *args)
487
+ if aliases[command]
488
+ Qcmd.debug "[CLI handle_input] using alias #{ command }"
489
+
490
+ new_expression = expand_alias(command, args)
491
+
492
+ # alias expansion failed, go back to CLI
493
+ return if new_expression.nil?
494
+
495
+ Qcmd.debug "[CLI handle_input] expanded to: #{ new_expression.inspect }"
496
+
497
+ # recurse!
498
+ if new_expression.size == 1 && new_expression[0].is_a?(Array)
499
+ while new_expression.size == 1 && new_expression[0].is_a?(Array)
500
+ new_expression = new_expression[0]
501
+ end
502
+ end
503
+
504
+ if new_expression.all? {|exp| exp.is_a?(Array)}
505
+ new_expression.each {|nested_expression|
506
+ handle_input nested_expression
507
+ }
230
508
  else
231
- if %r[/] =~ command
232
- # might be legit OSC command, try sending
233
- server.send_command(command, *args)
234
- else
235
- print "unrecognized command: #{ command }"
509
+ handle_input(new_expression)
510
+ end
511
+
512
+
513
+ elsif Qcmd.context.cue_connected? && Qcmd::InputCompleter::ReservedCueWords.include?(command)
514
+ # prepend the given command with a cue address
515
+ if Qcmd.context.cue.number.nil? || Qcmd.context.cue.number.size == 0
516
+ command = "cue_id/#{ Qcmd.context.cue.id }/#{ command }"
517
+ else
518
+ command = "cue/#{ Qcmd.context.cue.number }/#{ command }"
519
+ end
520
+
521
+ args = [command].push(*args[1..-1])
522
+
523
+ cue_action = Qcmd::CueAction.new(args)
524
+
525
+ reply = cue_action.evaluate
526
+
527
+ if reply.is_a?(QLab::Reply)
528
+ if !reply.status.nil?
529
+ print reply.status
236
530
  end
531
+ else
532
+ render_data reply
237
533
  end
534
+
535
+ # send_workspace_command(command, *args)
536
+ elsif Qcmd.context.workspace_connected? && Qcmd::InputCompleter::ReservedWorkspaceWords.include?(command)
537
+ send_workspace_command(command, *args)
538
+
238
539
  else
239
- handle_failed_workspace_command message
540
+ # failure modes?
541
+ if %r[/] =~ command
542
+ # might be legit OSC command, try sending
543
+ reply = Qcmd::Action.evaluate(args)
544
+ if reply.is_a?(QLab::Reply)
545
+ if !reply.status.nil?
546
+ print reply.status
547
+ end
548
+ else
549
+ render_data reply
550
+ end
551
+ else
552
+ if Qcmd.context.cue_connected?
553
+ # cue is connected, but command isn't a valid cue command
554
+ print_wrapped("Unrecognized command: '#{ command }'. Try one of these cue commands: #{ Qcmd::InputCompleter::ReservedCueWords.join(', ') }")
555
+ print 'or disconnect from the cue with ..'
556
+ elsif Qcmd.context.workspace_connected?
557
+ # workspace is connected, but command isn't a valid workspace command
558
+ print_wrapped("Unrecognized command: '#{ command }'. Try one of these workspace commands: #{ Qcmd::InputCompleter::ReservedWorkspaceWords.join(', ') }")
559
+ elsif Qcmd.context.machine_connected?
560
+ send_command(command, *args)
561
+ else
562
+ print 'you must connect to a machine before sending commands'
563
+ end
564
+ end
240
565
  end
241
566
  end
242
567
  end
243
568
 
244
569
  def handle_failed_workspace_command command
245
- print_wrapped(%[the command, "#{ command }" can't be processed yet. you must
570
+ print_wrapped(%[The command, "#{ command }" can't be processed yet. you must
246
571
  first connect to a machine and a workspace
247
572
  before issuing other commands.])
248
573
  end
574
+
575
+ def add_cues_to_list cue, list, level
576
+ cue.cues.each {|_c|
577
+ name = _c.name
578
+
579
+ if level > 0
580
+ name += " " + ("-" * level) + "|"
581
+ end
582
+
583
+ list << [_c.number, _c.id, name, _c.type]
584
+ add_cues_to_list(_c, list, level + 1) if _c.has_cues?
585
+ }
586
+ end
587
+
588
+ ### communication actions
589
+ private
590
+
591
+ def render_data data
592
+ if data.is_a?(Array) || data.is_a?(Hash)
593
+ begin
594
+ print JSON.pretty_generate(data)
595
+ rescue JSON::GeneratorError
596
+ Qcmd.debug "[CLI render_data] failed to JSON parse data: #{ data.inspect }"
597
+ print data.to_s
598
+ end
599
+ else
600
+ print data.to_s
601
+ end
602
+ end
603
+
604
+
605
+ def send_command command, *args
606
+ options = args.extract_options!
607
+
608
+ Qcmd.debug "[CLI send_command] building command from command, args, options: #{ command.inspect }, #{ args.inspect }, #{ options.inspect }"
609
+
610
+ # make sure command is valid OSC Address
611
+ if %r[^/] =~ command
612
+ address = command
613
+ else
614
+ address = "/#{ command }"
615
+ end
616
+
617
+ osc_message = OSC::Message.new address, *args
618
+
619
+ Qcmd.debug "[CLI send_command] sending osc message #{ osc_message.address } #{osc_message.has_arguments? ? 'with' : 'without'} args"
620
+
621
+ if block_given?
622
+ # use given response handler, pass it response as a QLab Reply
623
+ Qcmd.context.qlab.send osc_message do |response|
624
+ Qcmd.debug "[CLI send_command] converting OSC::Message to QLab::Reply"
625
+ yield QLab::Reply.new(response)
626
+ end
627
+ else
628
+ # rely on default response handler
629
+ Qcmd.context.qlab.send(osc_message)
630
+ end
631
+ end
632
+
633
+ def send_workspace_command _command, *args
634
+ command = "workspace/#{ Qcmd.context.workspace.id }/#{ _command }"
635
+ send_command(command, *args)
636
+ end
637
+
638
+ ## QLab commands
639
+
640
+ def load_cues
641
+ cues = Qcmd::Action.evaluate('/cueLists')
642
+ Qcmd.context.workspace.cue_lists = cues.map {|cue_list| Qcmd::QLab::CueList.new(cue_list)}
643
+ end
249
644
  end
250
645
  end