qcmd 0.1.6 → 0.1.7
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 +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
|