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 +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
|