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 +7 -2
- data/TODO.md +31 -0
- data/bin/qcmd +2 -1
- data/lib/qcmd.rb +15 -3
- data/lib/qcmd/action.rb +160 -0
- data/lib/qcmd/aliases.rb +25 -0
- data/lib/qcmd/cli.rb +503 -108
- data/lib/qcmd/commands.rb +198 -142
- data/lib/qcmd/configuration.rb +83 -0
- data/lib/qcmd/context.rb +19 -13
- data/lib/qcmd/core_ext/osc/tcp_client.rb +155 -0
- data/lib/qcmd/handler.rb +49 -67
- data/lib/qcmd/history.rb +26 -0
- data/lib/qcmd/input_completer.rb +12 -2
- data/lib/qcmd/network.rb +9 -3
- data/lib/qcmd/parser.rb +14 -83
- data/lib/qcmd/plaintext.rb +0 -4
- data/lib/qcmd/qlab.rb +1 -0
- data/lib/qcmd/qlab/cue.rb +20 -0
- data/lib/qcmd/qlab/cue_list.rb +83 -0
- data/lib/qcmd/qlab/reply.rb +18 -4
- data/lib/qcmd/qlab/workspace.rb +23 -1
- data/lib/qcmd/version.rb +1 -1
- data/lib/vendor/sexpistol/LICENSE +20 -0
- data/lib/vendor/sexpistol/sexpistol.rb +2 -0
- data/lib/vendor/sexpistol/sexpistol/sexpistol.rb +76 -0
- data/lib/vendor/sexpistol/sexpistol/sexpistol_parser.rb +94 -0
- data/sample/dnssd.rb +20 -3
- data/sample/simple_console.rb +186 -43
- data/sample/tcp_qlab_connection.rb +67 -0
- data/spec/unit/action_spec.rb +84 -0
- data/spec/unit/commands_spec.rb +135 -14
- data/spec/unit/parser_spec.rb +36 -5
- metadata +124 -122
- data/lib/qcmd/core_ext/osc/stopping_server.rb +0 -84
- data/lib/qcmd/server.rb +0 -175
- data/spec/unit/osc_server_spec.rb +0 -78
data/README.md
CHANGED
@@ -13,7 +13,12 @@ IT.**
|
|
13
13
|
|
14
14
|
## Installation
|
15
15
|
|
16
|
-
|
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/
|
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
|
data/lib/qcmd.rb
CHANGED
@@ -1,17 +1,21 @@
|
|
1
|
+
# communicate!
|
1
2
|
require 'socket'
|
2
3
|
require 'osc-ruby'
|
3
4
|
|
4
|
-
|
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/
|
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
|
|
data/lib/qcmd/action.rb
ADDED
@@ -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
|
data/lib/qcmd/aliases.rb
ADDED
@@ -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
|
data/lib/qcmd/cli.rb
CHANGED
@@ -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 :
|
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 "
|
19
|
-
# start local listening port
|
20
|
-
Qcmd.context = Qcmd::Context.new
|
15
|
+
Qcmd.debug "[CLI initialize] launching with options: #{options.inspect}"
|
21
16
|
|
22
|
-
|
17
|
+
Qcmd.context = Qcmd::Context.new
|
23
18
|
|
24
19
|
if options[:machine_given]
|
25
|
-
Qcmd.debug "
|
20
|
+
Qcmd.debug "[CLI initialize] autoconnecting to machine #{ options[:machine] }"
|
26
21
|
|
27
22
|
Qcmd.while_quiet do
|
28
|
-
connect_to_machine_by_name
|
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
|
30
|
+
connect_to_workspace_by_name(options[:workspace], options[:workspace_passcode])
|
34
31
|
end
|
35
32
|
|
36
33
|
if options[:command_given]
|
37
|
-
|
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
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
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
|
-
|
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
|
185
|
+
def connect_to_machine_by_name machine_name
|
71
186
|
if machine = Qcmd::Network.find(machine_name)
|
72
|
-
print "
|
73
|
-
connect machine
|
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 '
|
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
|
81
|
-
workspace
|
82
|
-
|
83
|
-
|
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 %[
|
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
|
-
|
93
|
-
if
|
94
|
-
|
95
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
271
|
+
prefix, char = get_prompt
|
272
|
+
|
273
|
+
Qcmd.print prefix
|
274
|
+
cli_input = Readline.readline(char, true)
|
109
275
|
|
110
|
-
if
|
111
|
-
Qcmd.debug "
|
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
|
-
|
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
|
-
|
120
|
-
|
121
|
-
command = args.
|
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 "
|
309
|
+
Qcmd.debug "[CLI handle_input] connect command received args: #{ args.inspect } :: #{ args.map {|a| a.class.to_s}.inspect}"
|
130
310
|
|
131
|
-
|
132
|
-
passcode = args.shift
|
311
|
+
machine_ident = args[1]
|
133
312
|
|
134
|
-
|
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
|
-
|
138
|
-
|
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 "
|
346
|
+
Qcmd.debug "[CLI handle_input] use command received args: #{ args.inspect }"
|
142
347
|
|
143
|
-
workspace_name = args
|
144
|
-
passcode = args
|
348
|
+
workspace_name = args[1]
|
349
|
+
passcode = args[2]
|
145
350
|
|
146
|
-
Qcmd.debug "
|
351
|
+
Qcmd.debug "[CLI handle_input] using workspace: #{ workspace_name.inspect }"
|
147
352
|
|
148
353
|
if workspace_name
|
149
|
-
|
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
|
-
|
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
|
-
|
400
|
+
handle_failed_workspace_command cli_input
|
168
401
|
return
|
169
402
|
end
|
170
403
|
|
171
404
|
# reload cues
|
172
|
-
|
405
|
+
load_cues
|
173
406
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
423
|
+
handle_failed_workspace_command cli_input
|
184
424
|
return
|
185
425
|
end
|
186
426
|
|
187
|
-
|
188
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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
|
-
|
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
|
-
|
212
|
-
|
464
|
+
if _cue
|
465
|
+
Qcmd.context.cue = _cue
|
466
|
+
Qcmd.context.cue_connected = true
|
213
467
|
|
214
|
-
|
215
|
-
|
216
|
-
return
|
468
|
+
Qcmd.context.cue.sync
|
469
|
+
end
|
217
470
|
end
|
218
471
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
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
|
228
|
-
|
229
|
-
|
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
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
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(%[
|
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
|