qcmd 0.1.6 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +5 -0
- data/lib/qcmd.rb +16 -4
- data/lib/qcmd/cli.rb +26 -4
- data/lib/qcmd/commands.rb +123 -4
- data/lib/qcmd/context.rb +13 -0
- data/lib/qcmd/core_ext/osc/stopping_server.rb +84 -0
- data/lib/qcmd/handler.rb +18 -10
- data/lib/qcmd/input_completer.rb +3 -14
- data/lib/qcmd/network.rb +2 -14
- data/lib/qcmd/osc.rb +1 -0
- data/lib/qcmd/plaintext.rb +54 -14
- data/lib/qcmd/server.rb +2 -8
- data/lib/qcmd/version.rb +1 -1
- data/qcmd.gemspec +1 -3
- data/sample/simple_console.rb +148 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/{cli_spec.rb → unit/cli_spec.rb} +0 -0
- data/spec/{commands_spec.rb → unit/commands_spec.rb} +0 -0
- data/spec/unit/osc_server_spec.rb +78 -0
- data/spec/{parser_spec.rb → unit/parser_spec.rb} +0 -0
- data/spec/{qcmd_spec.rb → unit/qcmd_spec.rb} +14 -2
- data/spec/{qlab_spec.rb → unit/qlab_spec.rb} +0 -0
- metadata +33 -52
data/README.md
CHANGED
@@ -4,6 +4,9 @@
|
|
4
4
|
QLab 3's new OSC interface. `qcmd` should be useable from any machine on
|
5
5
|
the same local network as the QLab workspace you intend to work with.
|
6
6
|
|
7
|
+
This project is OS X only and has been tested against Ruby 1.8.7-p358 (OS X
|
8
|
+
10.8 default), Ruby 1.8.7 REE 2012.02, and Ruby 1.9.3-p327.
|
9
|
+
|
7
10
|
**This project should be considered experimental. DO NOT RUN SHOWS WITH
|
8
11
|
IT.**
|
9
12
|
|
@@ -16,6 +19,7 @@ Install this gem to your machine by running the following command:
|
|
16
19
|
|
17
20
|
That should do ya.
|
18
21
|
|
22
|
+
|
19
23
|
## Starting the `qcmd` console.
|
20
24
|
|
21
25
|
Run the following command in a terminal window:
|
@@ -122,6 +126,7 @@ An example session might look like this:
|
|
122
126
|
> exit
|
123
127
|
exiting...
|
124
128
|
|
129
|
+
|
125
130
|
## Contributing
|
126
131
|
|
127
132
|
1. Fork it
|
data/lib/qcmd.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'osc-ruby'
|
3
|
+
|
1
4
|
require 'qcmd/version'
|
5
|
+
|
6
|
+
require 'qcmd/plaintext'
|
7
|
+
require 'qcmd/commands'
|
2
8
|
require 'qcmd/input_completer'
|
3
9
|
|
4
10
|
require 'qcmd/core_ext/array'
|
5
11
|
require 'qcmd/core_ext/osc/message'
|
12
|
+
require 'qcmd/core_ext/osc/stopping_server'
|
6
13
|
|
7
14
|
module Qcmd
|
8
|
-
# Your code goes here...
|
9
15
|
autoload :Handler, 'qcmd/handler'
|
10
16
|
autoload :Server, 'qcmd/server'
|
11
17
|
autoload :Context, 'qcmd/context'
|
@@ -14,8 +20,6 @@ module Qcmd
|
|
14
20
|
autoload :Machine, 'qcmd/machine'
|
15
21
|
autoload :Network, 'qcmd/network'
|
16
22
|
autoload :QLab, 'qcmd/qlab'
|
17
|
-
autoload :Plaintext, 'qcmd/plaintext'
|
18
|
-
autoload :Commands, 'qcmd/commands'
|
19
23
|
autoload :VERSION, 'qcmd/version'
|
20
24
|
|
21
25
|
class << self
|
@@ -33,13 +37,21 @@ module Qcmd
|
|
33
37
|
self.log_level = :warning
|
34
38
|
end
|
35
39
|
|
40
|
+
def silent!
|
41
|
+
self.log_level = :none
|
42
|
+
end
|
43
|
+
|
44
|
+
def silent?
|
45
|
+
self.log_level == :none
|
46
|
+
end
|
47
|
+
|
36
48
|
def quiet?
|
37
49
|
self.log_level == :warning
|
38
50
|
end
|
39
51
|
|
40
52
|
def while_quiet
|
41
53
|
previous_level = self.log_level
|
42
|
-
self.log_level = :warning
|
54
|
+
self.log_level = :warning
|
43
55
|
yield
|
44
56
|
self.log_level = previous_level
|
45
57
|
end
|
data/lib/qcmd/cli.rb
CHANGED
@@ -3,7 +3,6 @@ require 'qcmd/server'
|
|
3
3
|
require 'readline'
|
4
4
|
|
5
5
|
require 'osc-ruby'
|
6
|
-
require 'osc-ruby/em_server'
|
7
6
|
|
8
7
|
module Qcmd
|
9
8
|
class CLI
|
@@ -36,7 +35,7 @@ module Qcmd
|
|
36
35
|
|
37
36
|
if options[:command_given]
|
38
37
|
handle_message options[:command]
|
39
|
-
|
38
|
+
print %[sent command "#{ options[:command] }"]
|
40
39
|
exit 0
|
41
40
|
end
|
42
41
|
end
|
@@ -146,7 +145,22 @@ module Qcmd
|
|
146
145
|
|
147
146
|
Qcmd.debug "(using workspace: #{ workspace_name.inspect })"
|
148
147
|
|
149
|
-
|
148
|
+
if workspace_name
|
149
|
+
connect_to_workspace_by_name workspace_name, passcode
|
150
|
+
else
|
151
|
+
print "No workspace name given. The following workspaces are available:"
|
152
|
+
Qcmd.context.print_workspace_list
|
153
|
+
end
|
154
|
+
|
155
|
+
when 'help'
|
156
|
+
help_command = args.shift
|
157
|
+
|
158
|
+
if help_command.nil?
|
159
|
+
Qcmd::Commands::Help.print_all_commands
|
160
|
+
# print help according to current context
|
161
|
+
else
|
162
|
+
# print command specific help
|
163
|
+
end
|
150
164
|
|
151
165
|
when 'cues'
|
152
166
|
if !Qcmd.context.workspace_connected?
|
@@ -179,13 +193,21 @@ module Qcmd
|
|
179
193
|
print
|
180
194
|
print " > cue NUMBER COMMAND ARGUMENTS"
|
181
195
|
print
|
182
|
-
print_wrapped("available cue commands are: #{Qcmd::
|
196
|
+
print_wrapped("available cue commands are: #{Qcmd::Commands::ALL_CUE_COMMANDS.join(', ')}")
|
183
197
|
elsif cue_action.nil?
|
184
198
|
server.send_workspace_command("cue/#{ cue_number }")
|
185
199
|
else
|
186
200
|
server.send_cue_command(cue_number, cue_action, *args)
|
187
201
|
end
|
188
202
|
|
203
|
+
when 'workspaces'
|
204
|
+
if !Qcmd.context.machine_connected?
|
205
|
+
print 'cannot load workspaces until you are connected to a machine'
|
206
|
+
return
|
207
|
+
end
|
208
|
+
|
209
|
+
server.load_workspaces
|
210
|
+
|
189
211
|
when 'workspace'
|
190
212
|
workspace_command = args.shift
|
191
213
|
|
data/lib/qcmd/commands.rb
CHANGED
@@ -1,10 +1,34 @@
|
|
1
|
+
require 'qcmd/plaintext'
|
2
|
+
|
1
3
|
module Qcmd
|
2
4
|
module Commands
|
3
|
-
# Commands
|
5
|
+
# All Commands
|
6
|
+
#
|
7
|
+
# *_RESPONSE lists are commands that expect responses
|
8
|
+
#
|
9
|
+
|
10
|
+
NO_MACHINE_RESPONSE = %w(connect)
|
11
|
+
|
4
12
|
MACHINE_RESPONSE = %w(workspaces)
|
5
13
|
|
6
14
|
WORKSPACE_RESPONSE = %w(
|
7
|
-
cueLists selectedCues runningCues runningOrPausedCues
|
15
|
+
cueLists selectedCues runningCues runningOrPausedCues thump
|
16
|
+
)
|
17
|
+
|
18
|
+
WORKSPACE_NO_RESPONSE = %w(
|
19
|
+
go stop pause resume reset panic
|
20
|
+
)
|
21
|
+
|
22
|
+
ALL_WORKSPACE_COMMANDS = [WORKSPACE_RESPONSE + WORKSPACE_NO_RESPONSE]
|
23
|
+
|
24
|
+
# commands that take no args and do not respond
|
25
|
+
CUE_NO_RESPONSE = %w(
|
26
|
+
start stop pause resume load preview reset panic
|
27
|
+
)
|
28
|
+
|
29
|
+
# commands that take args but do not respond
|
30
|
+
CUE_ARG_NO_RESPONSE = %w(
|
31
|
+
loadAt
|
8
32
|
)
|
9
33
|
|
10
34
|
# commands that always expect a response
|
@@ -16,14 +40,29 @@ module Qcmd
|
|
16
40
|
basics children
|
17
41
|
)
|
18
42
|
|
19
|
-
# commands that expect a response if given without args
|
43
|
+
# commands that take args but expect a response if given without args
|
20
44
|
NO_ARG_CUE_RESPONSE = %w(
|
21
45
|
number name notes cueTargetNumber cueTargetId preWait duration
|
22
46
|
postWait continueMode flagged armed colorName
|
23
47
|
sliderLevel
|
24
48
|
)
|
25
49
|
|
50
|
+
# all cue commands that take arguments
|
51
|
+
CUE_ARG = CUE_ARG_NO_RESPONSE + NO_ARG_CUE_RESPONSE
|
52
|
+
|
53
|
+
ALL_CUE_COMMANDS = CUE_NO_RESPONSE +
|
54
|
+
CUE_ARG_NO_RESPONSE +
|
55
|
+
CUE_RESPONSE +
|
56
|
+
NO_ARG_CUE_RESPONSE
|
57
|
+
|
26
58
|
class << self
|
59
|
+
def no_machine_response_matcher
|
60
|
+
@no_machine_response_matcher ||= %r[(#{NO_MACHINE_RESPONSE.join('|')})]
|
61
|
+
end
|
62
|
+
def no_machine_response_match command
|
63
|
+
!!(no_machine_response_matcher =~ command)
|
64
|
+
end
|
65
|
+
|
27
66
|
def machine_response_matcher
|
28
67
|
@machine_response_matcher ||= %r[(#{MACHINE_RESPONSE.join('|')})]
|
29
68
|
end
|
@@ -63,7 +102,7 @@ module Qcmd
|
|
63
102
|
when :none
|
64
103
|
# shouldn't be dealing with OSC messages when unconnected to
|
65
104
|
# machine or workspace
|
66
|
-
response =
|
105
|
+
response = no_machine_response_match(address)
|
67
106
|
when :machine
|
68
107
|
# could be workspace or machine command
|
69
108
|
response = machine_response_match(address) ||
|
@@ -100,5 +139,85 @@ module Qcmd
|
|
100
139
|
/workspace/ =~ address && !(%r[cue/] =~ address || %r[cue_id/] =~ address)
|
101
140
|
end
|
102
141
|
end
|
142
|
+
|
143
|
+
module Help
|
144
|
+
class << self
|
145
|
+
|
146
|
+
def print_all_commands
|
147
|
+
Qcmd.print %[
|
148
|
+
#{Qcmd.centered_text(' Available Commands ', '-')}
|
149
|
+
|
150
|
+
exit
|
151
|
+
|
152
|
+
close qcmd
|
153
|
+
|
154
|
+
|
155
|
+
connect MACHINE_NAME
|
156
|
+
|
157
|
+
connect to the machine with name MACHINE_NAME
|
158
|
+
|
159
|
+
|
160
|
+
disconnect
|
161
|
+
|
162
|
+
disconnect from the current machine and workspace
|
163
|
+
|
164
|
+
|
165
|
+
use WORKSPACE_NAME [PASSCODE]
|
166
|
+
|
167
|
+
connect to the workspace with name WORKSPACE_NAME using passcode PASSCODE. A
|
168
|
+
passcode is only required if the workspace has one enabled.
|
169
|
+
|
170
|
+
|
171
|
+
workspaces
|
172
|
+
|
173
|
+
show a list of the available workspaces for the currently connected machine.
|
174
|
+
|
175
|
+
|
176
|
+
workspace COMMAND [VALUE]
|
177
|
+
|
178
|
+
pass the given COMMAND to the connected workspace. The following commands will
|
179
|
+
act on a workspace but will not return a value:
|
180
|
+
|
181
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::WORKSPACE_NO_RESPONSE.sort.join(', '), :indent => ' ').join("\n")}
|
182
|
+
|
183
|
+
And these commands will not act on a workspace, but will return a value
|
184
|
+
(usually a list of cues):
|
185
|
+
|
186
|
+
#{Qcmd.wrapped_text((Qcmd::Commands::WORKSPACE_RESPONSE - ['connect']).sort.join(', '), :indent => ' ').join("\n")}
|
187
|
+
|
188
|
+
* Pro Tip: once you are connected to a workspace, you can just use COMMAND
|
189
|
+
and leave off "workspace" to quickly send the given COMMAND to the connected
|
190
|
+
workspace.
|
191
|
+
|
192
|
+
|
193
|
+
cue NUMBER COMMAND [VALUE [ANOTHER_VALUE ...]]
|
194
|
+
|
195
|
+
send a command to the cue with the given NUMBER.
|
196
|
+
|
197
|
+
NUMBER can be a string or a number, depending on the command.
|
198
|
+
|
199
|
+
COMMAND can be one of:
|
200
|
+
|
201
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::ALL_CUE_COMMANDS.sort.join(', '), :indent => ' ').join("\n")}
|
202
|
+
|
203
|
+
Of those commands, only some accept a VALUE. The following commands, if given
|
204
|
+
a value, will update the cue:
|
205
|
+
|
206
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::CUE_ARG.sort.join(', '), :indent => ' ').join("\n")}
|
207
|
+
|
208
|
+
Some cues are Read-Only and will return information about a cue:
|
209
|
+
|
210
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::CUE_RESPONSE.sort.join(', '), :indent => ' ').join("\n")}
|
211
|
+
|
212
|
+
Finally, some commands act on a cue, but don't take a VALUE and don't
|
213
|
+
respond:
|
214
|
+
|
215
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::CUE_NO_RESPONSE.sort.join(', '), :indent => ' ').join("\n")}
|
216
|
+
|
217
|
+
]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
103
222
|
end
|
104
223
|
end
|
data/lib/qcmd/context.rb
CHANGED
@@ -33,5 +33,18 @@ module Qcmd
|
|
33
33
|
:workspace
|
34
34
|
end
|
35
35
|
end
|
36
|
+
|
37
|
+
def print_workspace_list
|
38
|
+
Qcmd.print Qcmd.centered_text(" Workspaces ", '-')
|
39
|
+
Qcmd.print
|
40
|
+
|
41
|
+
machine.workspaces.each_with_index do |ws, n|
|
42
|
+
Qcmd.print "#{ n + 1 }. #{ ws.name }#{ ws.passcode? ? ' [PROTECTED]' : ''}"
|
43
|
+
end
|
44
|
+
|
45
|
+
Qcmd.print
|
46
|
+
Qcmd.print_wrapped('Type `use "WORKSPACE_NAME" PASSCODE` to load a workspace. Passcode is optional.')
|
47
|
+
Qcmd.print
|
48
|
+
end
|
36
49
|
end
|
37
50
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module OSC
|
2
|
+
class StoppingServer < Server
|
3
|
+
def initialize *args
|
4
|
+
@state = :initialized
|
5
|
+
@port = args.first
|
6
|
+
super(*args)
|
7
|
+
end
|
8
|
+
|
9
|
+
def run
|
10
|
+
@state = :starting
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def stop
|
15
|
+
@state = :stopping
|
16
|
+
stop_detector
|
17
|
+
stop_dispatcher
|
18
|
+
end
|
19
|
+
|
20
|
+
def state
|
21
|
+
@state
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def stop_detector
|
27
|
+
# send listening port a "CLOSE" signal on the open UDP port
|
28
|
+
_closer = UDPSocket.new
|
29
|
+
_closer.connect('', @port)
|
30
|
+
_closer.puts "CLOSE-#{@port}"
|
31
|
+
_closer.close unless _closer.closed? || !_closer.respond_to?(:close)
|
32
|
+
end
|
33
|
+
|
34
|
+
def stop_dispatcher
|
35
|
+
@queue << :stop
|
36
|
+
end
|
37
|
+
|
38
|
+
def dispatcher
|
39
|
+
loop do
|
40
|
+
mesg = @queue.pop
|
41
|
+
dispatch_message( mesg )
|
42
|
+
end
|
43
|
+
rescue StopException
|
44
|
+
@state = :stopped
|
45
|
+
end
|
46
|
+
|
47
|
+
def dispatch_message message
|
48
|
+
if message.is_a?(Symbol) && message.to_s == 'stop'
|
49
|
+
raise StopException.new
|
50
|
+
end
|
51
|
+
|
52
|
+
super(message)
|
53
|
+
end
|
54
|
+
|
55
|
+
def detector
|
56
|
+
@state = :listening
|
57
|
+
|
58
|
+
loop do
|
59
|
+
osc_data, network = @socket.recvfrom( 16384 )
|
60
|
+
|
61
|
+
# quit if socket receives the close signal
|
62
|
+
if osc_data == "CLOSE-#{@port}"
|
63
|
+
@socket.close if !@socket.closed? && @socket.respond_to?(:close)
|
64
|
+
break
|
65
|
+
end
|
66
|
+
|
67
|
+
unpack_socket_receipt osc_data, network
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def unpack_socket_receipt osc_data, network
|
72
|
+
ip_info = Array.new
|
73
|
+
ip_info << network[1]
|
74
|
+
ip_info.concat(network[2].split('.'))
|
75
|
+
OSC::OSCPacket.messages_from_network( osc_data, ip_info ).each do |message|
|
76
|
+
@queue.push(message)
|
77
|
+
end
|
78
|
+
rescue EOFError
|
79
|
+
# pass
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class StopException < Exception; end
|
84
|
+
end
|
data/lib/qcmd/handler.rb
CHANGED
@@ -10,15 +10,7 @@ module Qcmd
|
|
10
10
|
Qcmd.context.machine.workspaces = reply.data.map {|ws| Qcmd::QLab::Workspace.new(ws)}
|
11
11
|
|
12
12
|
unless Qcmd.quiet?
|
13
|
-
|
14
|
-
print
|
15
|
-
Qcmd.context.machine.workspaces.each_with_index do |ws, n|
|
16
|
-
print "#{ n + 1 }. #{ ws.name }#{ ws.passcode? ? ' [PROTECTED]' : ''}"
|
17
|
-
end
|
18
|
-
|
19
|
-
print
|
20
|
-
print_wrapped('Type `use "WORKSPACE_NAME" PASSCODE` to load a workspace. Passcode is optional.')
|
21
|
-
print
|
13
|
+
Qcmd.context.print_workspace_list
|
22
14
|
end
|
23
15
|
|
24
16
|
when %r[/workspace/[^/]+/connect]
|
@@ -69,8 +61,24 @@ module Qcmd
|
|
69
61
|
if result.is_a?(String) || result.is_a?(Numeric)
|
70
62
|
print result
|
71
63
|
else
|
72
|
-
|
64
|
+
case reply.address
|
65
|
+
when %r[/basics]
|
66
|
+
keys = result.keys.sort
|
67
|
+
table(['Field Name', 'Value'], keys.map {|key|
|
68
|
+
[key, result[key]]
|
69
|
+
})
|
70
|
+
else
|
71
|
+
begin
|
72
|
+
print JSON.pretty_generate(result)
|
73
|
+
rescue JSON::GeneratorError
|
74
|
+
print result.to_s
|
75
|
+
end
|
76
|
+
end
|
73
77
|
end
|
78
|
+
|
79
|
+
when %r[/thump]
|
80
|
+
print reply.data
|
81
|
+
|
74
82
|
else
|
75
83
|
Qcmd.debug "(unrecognized message from QLab, cannot handle #{ reply.address })"
|
76
84
|
end
|
data/lib/qcmd/input_completer.rb
CHANGED
@@ -4,23 +4,12 @@ module Qcmd
|
|
4
4
|
module InputCompleter
|
5
5
|
# the commands listed here should represent every possible legal command
|
6
6
|
ReservedWords = %w[
|
7
|
-
connect exit quit workspace workspaces disconnect
|
7
|
+
connect exit quit workspace workspaces disconnect help
|
8
8
|
]
|
9
9
|
|
10
|
-
ReservedWorkspaceWords =
|
11
|
-
cueLists selectedCues runningCues runningOrPausedCues thump
|
12
|
-
go stop pause resume reset panic disconnect
|
13
|
-
]
|
10
|
+
ReservedWorkspaceWords = Qcmd::Commands::ALL_WORKSPACE_COMMANDS
|
14
11
|
|
15
|
-
ReservedCueWords =
|
16
|
-
cue stop pause resume load preview reset panic loadAt uniqueID
|
17
|
-
hasFileTargets hasCueTargets allowsEditingDuration isLoaded isRunning
|
18
|
-
isPaused isBroken preWaitElapsed actionElapsed postWaitElapsed
|
19
|
-
percentPreWaitElapsed percentActionElapsed percentPostWaitElapsed
|
20
|
-
type number name notes cueTargetNumber cueTargetId preWait duration
|
21
|
-
postWait continueMode flagged armed colorName basics children
|
22
|
-
sliderLevel sliderLevels
|
23
|
-
]
|
12
|
+
ReservedCueWords = Qcmd::Commands::ALL_CUE_COMMANDS
|
24
13
|
|
25
14
|
CompletionProc = Proc.new {|input|
|
26
15
|
# puts "input: #{ input }"
|
data/lib/qcmd/network.rb
CHANGED
@@ -2,7 +2,7 @@ require 'dnssd'
|
|
2
2
|
|
3
3
|
module Qcmd
|
4
4
|
class Network
|
5
|
-
BROWSE_TIMEOUT =
|
5
|
+
BROWSE_TIMEOUT = 2
|
6
6
|
|
7
7
|
class << self
|
8
8
|
attr_accessor :machines, :browse_thread
|
@@ -18,19 +18,7 @@ module Qcmd
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
changed = false
|
23
|
-
previous = 0
|
24
|
-
|
25
|
-
# sleep for BROWSE_TIMEOUT loops
|
26
|
-
while naps < BROWSE_TIMEOUT
|
27
|
-
sleep 0.2
|
28
|
-
naps += 1
|
29
|
-
|
30
|
-
if machines.size != previous
|
31
|
-
previous = machines.size
|
32
|
-
end
|
33
|
-
end
|
21
|
+
sleep BROWSE_TIMEOUT
|
34
22
|
|
35
23
|
Thread.kill(browse_thread) if browse_thread.alive?
|
36
24
|
end
|
data/lib/qcmd/osc.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'qcmd/osc/server'
|
data/lib/qcmd/plaintext.rb
CHANGED
@@ -8,17 +8,21 @@ module Qcmd
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
#
|
11
|
+
# display output unless absolutely silent
|
12
12
|
def print message=nil
|
13
|
-
log(message)
|
13
|
+
log(message) unless Qcmd.silent?
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_columns value
|
17
|
+
@columns = value
|
14
18
|
end
|
15
19
|
|
16
20
|
def columns
|
17
|
-
begin
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
21
|
+
@columns || (begin
|
22
|
+
@columns = `stty size`.split.last.to_i
|
23
|
+
rescue
|
24
|
+
@columns = 80
|
25
|
+
end)
|
22
26
|
end
|
23
27
|
|
24
28
|
def pluralize n, word
|
@@ -27,12 +31,35 @@ module Qcmd
|
|
27
31
|
|
28
32
|
def word_wrap(text, options={})
|
29
33
|
options = {
|
30
|
-
:line_width => columns
|
34
|
+
:line_width => columns,
|
35
|
+
:preserve_whitespace => false
|
31
36
|
}.merge options
|
32
37
|
|
33
|
-
|
34
|
-
|
35
|
-
end
|
38
|
+
unless options[:preserve_whitespace]
|
39
|
+
text = text.gsub(/\s+/, ' ') # collapse whitespace
|
40
|
+
end
|
41
|
+
|
42
|
+
prefix = options[:indent] ? options[:indent] : ''
|
43
|
+
|
44
|
+
line_width = options[:line_width]
|
45
|
+
lines = ['']
|
46
|
+
|
47
|
+
space = ' '
|
48
|
+
space_size = 2
|
49
|
+
space_left = line_width
|
50
|
+
text.split.each do |word|
|
51
|
+
if (word.size + space.size) >= space_left
|
52
|
+
word = "%s%s" % [prefix, word]
|
53
|
+
space_left = line_width - (word.size + space_size)
|
54
|
+
lines << ""
|
55
|
+
else
|
56
|
+
space_left = space_left - (word.size + space_size)
|
57
|
+
end
|
58
|
+
|
59
|
+
lines.last << "%s%s" % [word, space]
|
60
|
+
end
|
61
|
+
|
62
|
+
lines
|
36
63
|
end
|
37
64
|
|
38
65
|
def ascii_qlab
|
@@ -53,9 +80,14 @@ module Qcmd
|
|
53
80
|
end
|
54
81
|
|
55
82
|
# turn line into lines of text of columns length
|
56
|
-
def wrapped_text
|
57
|
-
|
58
|
-
|
83
|
+
def wrapped_text *args
|
84
|
+
options = {
|
85
|
+
:line_width => columns
|
86
|
+
}.merge args.extract_options!
|
87
|
+
|
88
|
+
line = args.shift
|
89
|
+
|
90
|
+
word_wrap(line, options)
|
59
91
|
end
|
60
92
|
|
61
93
|
def print_wrapped line
|
@@ -103,6 +135,14 @@ module Qcmd
|
|
103
135
|
def table headers, rows
|
104
136
|
print
|
105
137
|
columns = headers.map(&:size)
|
138
|
+
|
139
|
+
# coerce row values to strings
|
140
|
+
rows.each do |row|
|
141
|
+
columns.each_with_index do |col, n|
|
142
|
+
row[n] = row[n].to_s
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
106
146
|
rows.each do |row|
|
107
147
|
columns.each_with_index do |col, n|
|
108
148
|
columns[n] = [col, row[n].size].max + 1
|
data/lib/qcmd/server.rb
CHANGED
@@ -1,9 +1,4 @@
|
|
1
1
|
require 'osc-ruby'
|
2
|
-
require 'osc-ruby/em_server'
|
3
|
-
begin
|
4
|
-
require 'ruby-debug'
|
5
|
-
rescue LoadError
|
6
|
-
end
|
7
2
|
|
8
3
|
require 'json'
|
9
4
|
|
@@ -49,11 +44,10 @@ module Qcmd
|
|
49
44
|
# initialize
|
50
45
|
def listen
|
51
46
|
if receive_channel
|
52
|
-
Qcmd.debug "(stopping existing server)"
|
53
47
|
stop
|
54
48
|
end
|
55
49
|
|
56
|
-
self.receive_channel = OSC::
|
50
|
+
self.receive_channel = OSC::StoppingServer.new(self.receive_port)
|
57
51
|
|
58
52
|
Qcmd.debug "(opening receiving channel: #{ self.receive_channel.inspect })"
|
59
53
|
|
@@ -133,7 +127,7 @@ module Qcmd
|
|
133
127
|
end
|
134
128
|
|
135
129
|
def stop
|
136
|
-
|
130
|
+
receive_channel.stop if receive_channel && receive_channel.state == :listening
|
137
131
|
end
|
138
132
|
|
139
133
|
def run
|
data/lib/qcmd/version.rb
CHANGED
data/qcmd.gemspec
CHANGED
@@ -18,13 +18,11 @@ Gem::Specification.new do |gem|
|
|
18
18
|
gem.require_paths = ["lib"]
|
19
19
|
|
20
20
|
gem.add_runtime_dependency 'dnssd'
|
21
|
-
gem.add_runtime_dependency 'eventmachine'
|
22
21
|
gem.add_runtime_dependency 'json'
|
23
22
|
gem.add_runtime_dependency 'osc-ruby'
|
24
23
|
gem.add_runtime_dependency 'trollop'
|
25
24
|
|
26
|
-
gem.add_development_dependency "rspec"
|
25
|
+
gem.add_development_dependency "rspec", '~> 2.10.0'
|
27
26
|
gem.add_development_dependency "cucumber"
|
28
27
|
gem.add_development_dependency "aruba"
|
29
|
-
gem.add_development_dependency 'ruby-debug'
|
30
28
|
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# ruby builtin
|
4
|
+
require 'readline'
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
|
8
|
+
# use Qcmd's parser for type conversions and double quote recognizing
|
9
|
+
require 'qcmd/parser'
|
10
|
+
|
11
|
+
# other gems
|
12
|
+
require 'osc-ruby'
|
13
|
+
require 'json'
|
14
|
+
|
15
|
+
# handle Ctrl-C quitting
|
16
|
+
trap("INT") { exit }
|
17
|
+
|
18
|
+
# if there are args, there must be two:
|
19
|
+
#
|
20
|
+
# send_address:send_port
|
21
|
+
#
|
22
|
+
# and
|
23
|
+
#
|
24
|
+
# receive_port
|
25
|
+
|
26
|
+
# default qlab send port 53000
|
27
|
+
send_address = 'localhost'
|
28
|
+
send_port = 53000
|
29
|
+
|
30
|
+
# default qlab receive port 53001
|
31
|
+
receive_port = 53001
|
32
|
+
|
33
|
+
if ARGV.size > 0
|
34
|
+
send_matcher = /([^:]+):(\d+)/
|
35
|
+
recv_matcher = /(\d+)/
|
36
|
+
|
37
|
+
if send_matcher =~ ARGV[0]
|
38
|
+
send_address, send_port = $1, $2
|
39
|
+
elsif recv_matcher =~ ARGV[0]
|
40
|
+
receive_port = $1
|
41
|
+
else
|
42
|
+
puts 'send address must be an address in the form SERVER_ADDRESS:PORT'
|
43
|
+
end
|
44
|
+
|
45
|
+
if ARGV[1]
|
46
|
+
if recv_matcher =~ ARGV[1]
|
47
|
+
receive_port = $1
|
48
|
+
else
|
49
|
+
puts 'send address must be a port number'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
puts %[connecting to server #{send_address}:#{send_port} with receiver at port #{receive_port}]
|
55
|
+
|
56
|
+
# how long to wait for responses from QLab. If you notice responses coming in
|
57
|
+
# out of order, you may need to increase this value.
|
58
|
+
REPLY_TIMEOUT = 1
|
59
|
+
|
60
|
+
# open IO pipes to communicate between client / server process
|
61
|
+
response_receiver, writer = IO.pipe
|
62
|
+
|
63
|
+
# fork readline process to allow server to communicate because if we use
|
64
|
+
# Thread.new, readline locks the WHOLE Ruby VM and the server can't start
|
65
|
+
pid = fork do
|
66
|
+
# close the IO channel that server process will be using
|
67
|
+
writer.close
|
68
|
+
|
69
|
+
# native OSC connection, outbound
|
70
|
+
client = OSC::Client.new 'localhost', send_port
|
71
|
+
|
72
|
+
loop do
|
73
|
+
command_string = Readline.readline('> ', true)
|
74
|
+
next if command_string.nil? || command_string.strip.size == 0
|
75
|
+
|
76
|
+
# break command string up and properly typecast all given values
|
77
|
+
args = Qcmd::Parser.parse(command_string)
|
78
|
+
address = args.shift
|
79
|
+
|
80
|
+
# quit, q, and exit all quit
|
81
|
+
exit if /^(q(uit)?|exit)/i =~ address
|
82
|
+
|
83
|
+
# "sanitize" the given address
|
84
|
+
if %r[^/] != address
|
85
|
+
if address == '>'
|
86
|
+
# pasted previous command line entry
|
87
|
+
address = args.shift
|
88
|
+
else
|
89
|
+
# add lazy slash
|
90
|
+
address = "/#{ address }"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
message = OSC::Message.new(address, *args)
|
95
|
+
client.send message
|
96
|
+
|
97
|
+
# wait for response until TIMEOUT seconds
|
98
|
+
select = IO.select([response_receiver], [], [], REPLY_TIMEOUT)
|
99
|
+
if !select.nil?
|
100
|
+
rs = select[0]
|
101
|
+
|
102
|
+
# get readable channel
|
103
|
+
if in_channel = rs[0]
|
104
|
+
# read everything until end of stream
|
105
|
+
while line = in_channel.gets
|
106
|
+
if line.strip != '<<EOS>>'
|
107
|
+
puts line
|
108
|
+
else
|
109
|
+
break
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
else
|
114
|
+
# select timed out, probably not going to get a response,
|
115
|
+
# go back to command line mode
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
puts "launched console with process id #{ pid }, use Ctrl-c or 'exit' to quit"
|
121
|
+
|
122
|
+
# close unused pipe
|
123
|
+
response_receiver.close
|
124
|
+
|
125
|
+
# native OSC connection, inbound
|
126
|
+
server = OSC::Server.new receive_port
|
127
|
+
|
128
|
+
# server listens and forwards responses to the forked process
|
129
|
+
server.add_method %r[/reply] do |osc_message|
|
130
|
+
data = JSON.parse(osc_message.to_a.first)['data']
|
131
|
+
|
132
|
+
begin
|
133
|
+
writer.puts JSON.pretty_generate(data)
|
134
|
+
rescue JSON::GeneratorError
|
135
|
+
writer.puts data.to_s
|
136
|
+
end
|
137
|
+
|
138
|
+
# end of signal
|
139
|
+
writer.puts '<<EOS>>'
|
140
|
+
end
|
141
|
+
|
142
|
+
# start blocking server
|
143
|
+
Thread.new do
|
144
|
+
server.run
|
145
|
+
end
|
146
|
+
|
147
|
+
# chill until the command line process quits
|
148
|
+
Process.wait pid
|
data/spec/spec_helper.rb
ADDED
File without changes
|
File without changes
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require File.join( File.dirname(__FILE__) , '..', 'spec_helper' )
|
2
|
+
require 'qcmd'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
class PortFactory
|
6
|
+
@@counter = 12345
|
7
|
+
|
8
|
+
def self.new_port
|
9
|
+
@@counter += 1
|
10
|
+
@@counter
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe OSC::StoppingServer do
|
15
|
+
before :each do
|
16
|
+
@port = PortFactory.new_port
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should bind to a socket when initialized" do
|
20
|
+
UDPSocket.any_instance.should_receive(:bind).with('', @port)
|
21
|
+
server = OSC::StoppingServer.new @port
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should start a listening thread when started' do
|
25
|
+
server = OSC::StoppingServer.new @port
|
26
|
+
|
27
|
+
test_thread = Thread.new do
|
28
|
+
Thread.should_receive :fork
|
29
|
+
server.run
|
30
|
+
end
|
31
|
+
|
32
|
+
server.stop
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should kill the listening thread and close socket when stopped' do
|
36
|
+
server = OSC::StoppingServer.new @port
|
37
|
+
|
38
|
+
test_thread = Thread.new do
|
39
|
+
server.run
|
40
|
+
end
|
41
|
+
|
42
|
+
sleep 0.1
|
43
|
+
server.stop
|
44
|
+
sleep 0.1
|
45
|
+
|
46
|
+
# server has stopped blocking
|
47
|
+
test_thread.alive?.should == false
|
48
|
+
|
49
|
+
# server claims it is closed
|
50
|
+
server.state.should == :stopped
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should create messages for legitimate OSC commands' do
|
54
|
+
server = OSC::StoppingServer.new @port
|
55
|
+
|
56
|
+
received = nil
|
57
|
+
|
58
|
+
server.add_method '/test' do |message|
|
59
|
+
received = message
|
60
|
+
end
|
61
|
+
|
62
|
+
test_thread = Thread.new do
|
63
|
+
server.run
|
64
|
+
end
|
65
|
+
|
66
|
+
received.should == nil
|
67
|
+
|
68
|
+
client = OSC::Client.new 'localhost', @port
|
69
|
+
client.send OSC::Message.new('/test', 'ansible')
|
70
|
+
|
71
|
+
sleep 0.1
|
72
|
+
server.stop
|
73
|
+
|
74
|
+
received.is_a?(OSC::Message).should == true
|
75
|
+
received.to_a.first.should == 'ansible'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
File without changes
|
@@ -3,8 +3,7 @@ require 'qcmd'
|
|
3
3
|
describe Qcmd do
|
4
4
|
# tests go here
|
5
5
|
it "should log debug messages when in verbose mode" do
|
6
|
-
Qcmd.should_receive(:log
|
7
|
-
|
6
|
+
Qcmd.should_receive(:log).with('hello')
|
8
7
|
Qcmd.verbose!
|
9
8
|
Qcmd.log_level.should eql(:debug)
|
10
9
|
Qcmd.debug 'hello'
|
@@ -16,4 +15,17 @@ describe Qcmd do
|
|
16
15
|
Qcmd.log_level.should eql(:warning)
|
17
16
|
Qcmd.debug 'hello'
|
18
17
|
end
|
18
|
+
|
19
|
+
it 'should not log debug messages when in quiet block' do
|
20
|
+
Qcmd.verbose!
|
21
|
+
Qcmd.log_level.should eql(:debug)
|
22
|
+
|
23
|
+
Qcmd.while_quiet do
|
24
|
+
Qcmd.should_not_receive(:log)
|
25
|
+
Qcmd.log_level.should eql(:warning)
|
26
|
+
Qcmd.debug 'hello'
|
27
|
+
end
|
28
|
+
|
29
|
+
Qcmd.log_level.should eql(:debug)
|
30
|
+
end
|
19
31
|
end
|
File without changes
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qcmd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 21
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 7
|
10
|
+
version: 0.1.7
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Adam Bachman
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-11-
|
18
|
+
date: 2012-11-24 00:00:00 -05:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -33,7 +33,7 @@ dependencies:
|
|
33
33
|
type: :runtime
|
34
34
|
version_requirements: *id001
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
|
-
name:
|
36
|
+
name: json
|
37
37
|
prerelease: false
|
38
38
|
requirement: &id002 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
@@ -47,7 +47,7 @@ dependencies:
|
|
47
47
|
type: :runtime
|
48
48
|
version_requirements: *id002
|
49
49
|
- !ruby/object:Gem::Dependency
|
50
|
-
name:
|
50
|
+
name: osc-ruby
|
51
51
|
prerelease: false
|
52
52
|
requirement: &id003 !ruby/object:Gem::Requirement
|
53
53
|
none: false
|
@@ -61,7 +61,7 @@ dependencies:
|
|
61
61
|
type: :runtime
|
62
62
|
version_requirements: *id003
|
63
63
|
- !ruby/object:Gem::Dependency
|
64
|
-
name:
|
64
|
+
name: trollop
|
65
65
|
prerelease: false
|
66
66
|
requirement: &id004 !ruby/object:Gem::Requirement
|
67
67
|
none: false
|
@@ -75,21 +75,23 @@ dependencies:
|
|
75
75
|
type: :runtime
|
76
76
|
version_requirements: *id004
|
77
77
|
- !ruby/object:Gem::Dependency
|
78
|
-
name:
|
78
|
+
name: rspec
|
79
79
|
prerelease: false
|
80
80
|
requirement: &id005 !ruby/object:Gem::Requirement
|
81
81
|
none: false
|
82
82
|
requirements:
|
83
|
-
- -
|
83
|
+
- - ~>
|
84
84
|
- !ruby/object:Gem::Version
|
85
|
-
hash:
|
85
|
+
hash: 39
|
86
86
|
segments:
|
87
|
+
- 2
|
88
|
+
- 10
|
87
89
|
- 0
|
88
|
-
version:
|
89
|
-
type: :
|
90
|
+
version: 2.10.0
|
91
|
+
type: :development
|
90
92
|
version_requirements: *id005
|
91
93
|
- !ruby/object:Gem::Dependency
|
92
|
-
name:
|
94
|
+
name: cucumber
|
93
95
|
prerelease: false
|
94
96
|
requirement: &id006 !ruby/object:Gem::Requirement
|
95
97
|
none: false
|
@@ -103,7 +105,7 @@ dependencies:
|
|
103
105
|
type: :development
|
104
106
|
version_requirements: *id006
|
105
107
|
- !ruby/object:Gem::Dependency
|
106
|
-
name:
|
108
|
+
name: aruba
|
107
109
|
prerelease: false
|
108
110
|
requirement: &id007 !ruby/object:Gem::Requirement
|
109
111
|
none: false
|
@@ -116,34 +118,6 @@ dependencies:
|
|
116
118
|
version: "0"
|
117
119
|
type: :development
|
118
120
|
version_requirements: *id007
|
119
|
-
- !ruby/object:Gem::Dependency
|
120
|
-
name: aruba
|
121
|
-
prerelease: false
|
122
|
-
requirement: &id008 !ruby/object:Gem::Requirement
|
123
|
-
none: false
|
124
|
-
requirements:
|
125
|
-
- - ">="
|
126
|
-
- !ruby/object:Gem::Version
|
127
|
-
hash: 3
|
128
|
-
segments:
|
129
|
-
- 0
|
130
|
-
version: "0"
|
131
|
-
type: :development
|
132
|
-
version_requirements: *id008
|
133
|
-
- !ruby/object:Gem::Dependency
|
134
|
-
name: ruby-debug
|
135
|
-
prerelease: false
|
136
|
-
requirement: &id009 !ruby/object:Gem::Requirement
|
137
|
-
none: false
|
138
|
-
requirements:
|
139
|
-
- - ">="
|
140
|
-
- !ruby/object:Gem::Version
|
141
|
-
hash: 3
|
142
|
-
segments:
|
143
|
-
- 0
|
144
|
-
version: "0"
|
145
|
-
type: :development
|
146
|
-
version_requirements: *id009
|
147
121
|
description: A simple interactive QLab 3 command line controller
|
148
122
|
email:
|
149
123
|
- adam.bachman@gmail.com
|
@@ -168,10 +142,12 @@ files:
|
|
168
142
|
- lib/qcmd/context.rb
|
169
143
|
- lib/qcmd/core_ext/array.rb
|
170
144
|
- lib/qcmd/core_ext/osc/message.rb
|
145
|
+
- lib/qcmd/core_ext/osc/stopping_server.rb
|
171
146
|
- lib/qcmd/handler.rb
|
172
147
|
- lib/qcmd/input_completer.rb
|
173
148
|
- lib/qcmd/machine.rb
|
174
149
|
- lib/qcmd/network.rb
|
150
|
+
- lib/qcmd/osc.rb
|
175
151
|
- lib/qcmd/parser.rb
|
176
152
|
- lib/qcmd/plaintext.rb
|
177
153
|
- lib/qcmd/qlab.rb
|
@@ -182,11 +158,14 @@ files:
|
|
182
158
|
- lib/qcmd/version.rb
|
183
159
|
- qcmd.gemspec
|
184
160
|
- sample/dnssd.rb
|
185
|
-
-
|
186
|
-
- spec/
|
187
|
-
- spec/
|
188
|
-
- spec/
|
189
|
-
- spec/
|
161
|
+
- sample/simple_console.rb
|
162
|
+
- spec/spec_helper.rb
|
163
|
+
- spec/unit/cli_spec.rb
|
164
|
+
- spec/unit/commands_spec.rb
|
165
|
+
- spec/unit/osc_server_spec.rb
|
166
|
+
- spec/unit/parser_spec.rb
|
167
|
+
- spec/unit/qcmd_spec.rb
|
168
|
+
- spec/unit/qlab_spec.rb
|
190
169
|
has_rdoc: true
|
191
170
|
homepage: https://github.com/abachman/qcmd
|
192
171
|
licenses: []
|
@@ -223,8 +202,10 @@ specification_version: 3
|
|
223
202
|
summary: QLab 3 console
|
224
203
|
test_files:
|
225
204
|
- features/support/setup.rb
|
226
|
-
- spec/
|
227
|
-
- spec/
|
228
|
-
- spec/
|
229
|
-
- spec/
|
230
|
-
- spec/
|
205
|
+
- spec/spec_helper.rb
|
206
|
+
- spec/unit/cli_spec.rb
|
207
|
+
- spec/unit/commands_spec.rb
|
208
|
+
- spec/unit/osc_server_spec.rb
|
209
|
+
- spec/unit/parser_spec.rb
|
210
|
+
- spec/unit/qcmd_spec.rb
|
211
|
+
- spec/unit/qlab_spec.rb
|