qcmd 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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