qcmd 0.1.0
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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +131 -0
- data/Rakefile +14 -0
- data/TODO.md +3 -0
- data/bin/qcmd +31 -0
- data/features/hello.feature +13 -0
- data/features/support/setup.rb +2 -0
- data/lib/qcmd.rb +48 -0
- data/lib/qcmd/cli.rb +167 -0
- data/lib/qcmd/commands.rb +103 -0
- data/lib/qcmd/context.rb +37 -0
- data/lib/qcmd/core_ext/array.rb +6 -0
- data/lib/qcmd/core_ext/osc/message.rb +7 -0
- data/lib/qcmd/handler.rb +80 -0
- data/lib/qcmd/input_completer.rb +67 -0
- data/lib/qcmd/machine.rb +27 -0
- data/lib/qcmd/network.rb +70 -0
- data/lib/qcmd/parser.rb +48 -0
- data/lib/qcmd/plaintext.rb +116 -0
- data/lib/qcmd/qlab.rb +3 -0
- data/lib/qcmd/qlab/cue.rb +70 -0
- data/lib/qcmd/qlab/reply.rb +25 -0
- data/lib/qcmd/qlab/workspace.rb +28 -0
- data/lib/qcmd/server.rb +178 -0
- data/lib/qcmd/version.rb +3 -0
- data/qcmd.gemspec +30 -0
- data/sample/dnssd.rb +36 -0
- data/spec/cli_spec.rb +14 -0
- data/spec/commands_spec.rb +38 -0
- data/spec/qcmd_spec.rb +19 -0
- data/spec/qlab_spec.rb +15 -0
- metadata +230 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
module Qcmd
|
2
|
+
module Plaintext
|
3
|
+
def log message=nil
|
4
|
+
if message
|
5
|
+
puts message
|
6
|
+
else
|
7
|
+
puts
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# always output
|
12
|
+
def print message=nil
|
13
|
+
log(message)
|
14
|
+
end
|
15
|
+
|
16
|
+
def columns
|
17
|
+
begin
|
18
|
+
`stty size`.split.last.to_i
|
19
|
+
rescue
|
20
|
+
80
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def pluralize n, word
|
25
|
+
"#{n} #{n == 1 ? word : word + 's'}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def word_wrap(text, options={})
|
29
|
+
options = {
|
30
|
+
:line_width => columns
|
31
|
+
}.merge options
|
32
|
+
|
33
|
+
text.split("\n").collect do |line|
|
34
|
+
line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line
|
35
|
+
end * "\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
def ascii_qlab
|
39
|
+
[' .:::: .:: .:: ',
|
40
|
+
' .:: .:: .:: .:: ',
|
41
|
+
'.:: .::.:: .:: .:: ',
|
42
|
+
'.:: .::.:: .:: .:: .:: .:: ',
|
43
|
+
'.:: .::.:: .:: .:: .:: .::',
|
44
|
+
' .:: .: .:: .:: .:: .:: .:: .::',
|
45
|
+
' .:: :: .:::::::: .:: .:::.:: .:: ',
|
46
|
+
' .: '].map {|line|
|
47
|
+
print centered_text(line)
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def joined_wrapped_text line
|
52
|
+
wrapped_text(line).join "\n"
|
53
|
+
end
|
54
|
+
|
55
|
+
# turn line into lines of text of columns length
|
56
|
+
def wrapped_text line
|
57
|
+
word_wrap(line, :line_width => columns).split("\n")
|
58
|
+
end
|
59
|
+
|
60
|
+
def right_text line
|
61
|
+
diff = [(columns - line.size), 0].max
|
62
|
+
"%s%s" % [' ' * diff, line]
|
63
|
+
end
|
64
|
+
|
65
|
+
def centered_text line, char=' '
|
66
|
+
if line.size > columns && line.split(' ').all? {|chunk| chunk.size < columns}
|
67
|
+
# wrap the text then center each line, then join
|
68
|
+
return wrapped_text(line).map {|l| centered_text(l, char)}.join("\n")
|
69
|
+
end
|
70
|
+
|
71
|
+
diff = (columns - line.size)
|
72
|
+
|
73
|
+
return line if diff < 0
|
74
|
+
|
75
|
+
lpad = diff / 2
|
76
|
+
rpad = diff - lpad
|
77
|
+
|
78
|
+
"%s%s%s" % [char * lpad, line, char * rpad]
|
79
|
+
end
|
80
|
+
|
81
|
+
def split_text left, right
|
82
|
+
diff = columns - left.size
|
83
|
+
if (diff - right.size) < 0
|
84
|
+
left_lines = wrapped_text(left)
|
85
|
+
diff = columns - left_lines.last.size
|
86
|
+
|
87
|
+
# still?
|
88
|
+
if (diff - right.size) < 0
|
89
|
+
diff = ''
|
90
|
+
right = "\n" + right_text(right)
|
91
|
+
end
|
92
|
+
|
93
|
+
left = left_lines.join "\n"
|
94
|
+
end
|
95
|
+
"%s%#{diff}s" % [left, right]
|
96
|
+
end
|
97
|
+
|
98
|
+
def table headers, rows
|
99
|
+
print
|
100
|
+
columns = headers.map(&:size)
|
101
|
+
rows.each do |row|
|
102
|
+
columns.each_with_index do |col, n|
|
103
|
+
columns[n] = [col, row[n].size].max + 1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
row_format = columns.map {|n| "%#{n}s\t"}.join('')
|
108
|
+
print row_format % headers
|
109
|
+
print
|
110
|
+
rows.each do |row|
|
111
|
+
print row_format % row
|
112
|
+
end
|
113
|
+
print
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/qcmd/qlab.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Qcmd
|
2
|
+
module QLab
|
3
|
+
# All return an array of cue dictionaries:
|
4
|
+
#
|
5
|
+
# [
|
6
|
+
# {
|
7
|
+
# "uniqueID": string,
|
8
|
+
# "number": string
|
9
|
+
# "name": string
|
10
|
+
# "type": string
|
11
|
+
# "colorName": string
|
12
|
+
# "flagged": number
|
13
|
+
# "armed": number
|
14
|
+
# }
|
15
|
+
# ]
|
16
|
+
# If the cue is a group, the dictionary will include an array of cue dictionaries for all children in the group:
|
17
|
+
#
|
18
|
+
# [
|
19
|
+
# {
|
20
|
+
# "uniqueID": string,
|
21
|
+
# "number": string
|
22
|
+
# "name": string
|
23
|
+
# "type": string
|
24
|
+
# "colorName": string
|
25
|
+
# "flagged": number
|
26
|
+
# "armed": number
|
27
|
+
# "cues": [ { }, { }, { } ]
|
28
|
+
# }
|
29
|
+
# ]
|
30
|
+
#
|
31
|
+
# [{\"number\":\"\",
|
32
|
+
# \"uniqueID\":\"1\",
|
33
|
+
# \"cues\":[{\"number\":\"1\",
|
34
|
+
# \"uniqueID\":\"2\",
|
35
|
+
# \"flagged\":false,
|
36
|
+
# \"type\":\"Wait\",
|
37
|
+
# \"colorName\":\"none\",
|
38
|
+
# \"name\":\"boom\",
|
39
|
+
# \"armed\":true}],
|
40
|
+
# \"flagged\":false,
|
41
|
+
# \"type\":\"Group\",
|
42
|
+
# \"colorName\":\"none\",
|
43
|
+
# \"name\":\"Main Cue List\",
|
44
|
+
# \"armed\":true}]
|
45
|
+
|
46
|
+
class Cue
|
47
|
+
attr_accessor :data
|
48
|
+
|
49
|
+
def initialize options={}
|
50
|
+
self.data = options
|
51
|
+
end
|
52
|
+
|
53
|
+
def id
|
54
|
+
data['uniqueID']
|
55
|
+
end
|
56
|
+
|
57
|
+
def name
|
58
|
+
data['name']
|
59
|
+
end
|
60
|
+
|
61
|
+
def number
|
62
|
+
data['number']
|
63
|
+
end
|
64
|
+
|
65
|
+
def type
|
66
|
+
data['type']
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Qcmd
|
2
|
+
module QLab
|
3
|
+
class Reply < Struct.new(:osc_message)
|
4
|
+
def json
|
5
|
+
@json ||= JSON.parse(osc_message.to_a.first)
|
6
|
+
end
|
7
|
+
|
8
|
+
def address
|
9
|
+
@address ||= json['address']
|
10
|
+
end
|
11
|
+
|
12
|
+
def data
|
13
|
+
@data ||= json['data']
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_cue_command?
|
17
|
+
Qcmd::Commands.is_cue_command?(address)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"<Qcmd::Qlab::Reply address:'#{address}' data:#{data.inspect}>"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Qcmd
|
2
|
+
module QLab
|
3
|
+
#
|
4
|
+
# "uniqueID": string,
|
5
|
+
# "displayName": string
|
6
|
+
# "hasPasscode": number
|
7
|
+
#
|
8
|
+
class Workspace
|
9
|
+
attr_accessor :data, :passcode, :cue_lists, :cues
|
10
|
+
|
11
|
+
def initialize options={}
|
12
|
+
self.data = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
data['displayName']
|
17
|
+
end
|
18
|
+
|
19
|
+
def passcode?
|
20
|
+
!!data['hasPasscode']
|
21
|
+
end
|
22
|
+
|
23
|
+
def id
|
24
|
+
data['uniqueID']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/qcmd/server.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'osc-ruby'
|
2
|
+
require 'osc-ruby/em_server'
|
3
|
+
require 'ruby-debug'
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Qcmd
|
8
|
+
class TimeoutError < Exception; end
|
9
|
+
|
10
|
+
class Server
|
11
|
+
attr_accessor :receive_channel, :receive_thread, :receive_port, :send_channel, :machine
|
12
|
+
|
13
|
+
def initialize *args
|
14
|
+
options = args.extract_options!
|
15
|
+
|
16
|
+
self.receive_port = options[:receive]
|
17
|
+
connect_to_client
|
18
|
+
|
19
|
+
@handler = Qcmd::Handler.new
|
20
|
+
@sent_messages = []
|
21
|
+
@sent_messages_expecting_reply = []
|
22
|
+
@received_messages = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def connect_to_client
|
26
|
+
self.machine = Qcmd.context.machine
|
27
|
+
self.send_channel = OSC::Client.new machine.address, machine.port
|
28
|
+
|
29
|
+
Qcmd.debug '(setting up listening connection)'
|
30
|
+
listen
|
31
|
+
end
|
32
|
+
|
33
|
+
def generic_responding_proc
|
34
|
+
proc do |osc_message|
|
35
|
+
@received_messages << osc_message
|
36
|
+
|
37
|
+
begin
|
38
|
+
Qcmd.debug "(received message: #{ osc_message.address })"
|
39
|
+
reply_received QLab::Reply.new(osc_message)
|
40
|
+
rescue => ex
|
41
|
+
Qcmd.debug "(ERROR #{ ex.message })"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# initialize
|
47
|
+
def listen
|
48
|
+
if receive_channel
|
49
|
+
Qcmd.debug "(stopping existing server)"
|
50
|
+
stop
|
51
|
+
end
|
52
|
+
|
53
|
+
self.receive_channel = OSC::EMServer.new(self.receive_port)
|
54
|
+
|
55
|
+
Qcmd.debug "(opening receiving channel: #{ self.receive_channel.inspect })"
|
56
|
+
|
57
|
+
receive_channel.add_method %r{/reply/?(.*)}, &generic_responding_proc
|
58
|
+
end
|
59
|
+
|
60
|
+
def replies_expected?
|
61
|
+
@sent_messages_expecting_reply.size > 0
|
62
|
+
end
|
63
|
+
|
64
|
+
def reply_received reply
|
65
|
+
Qcmd.debug "(receiving #{ reply })"
|
66
|
+
|
67
|
+
# update world state
|
68
|
+
begin
|
69
|
+
@handler.handle reply
|
70
|
+
rescue => ex
|
71
|
+
print "(ERROR: #{ ex.message })"
|
72
|
+
end
|
73
|
+
|
74
|
+
# FIFO
|
75
|
+
@sent_messages_expecting_reply.shift
|
76
|
+
|
77
|
+
Qcmd.debug "(#{ @sent_messages_expecting_reply.size } messages awaiting reply)"
|
78
|
+
end
|
79
|
+
|
80
|
+
def wait_for_replies
|
81
|
+
begin
|
82
|
+
yield
|
83
|
+
|
84
|
+
naps = 0
|
85
|
+
while replies_expected? do
|
86
|
+
if naps > 20
|
87
|
+
# FAILED TO GET RESPONSE
|
88
|
+
raise TimeoutError.new
|
89
|
+
end
|
90
|
+
|
91
|
+
naps += 1
|
92
|
+
sleep 0.1
|
93
|
+
end
|
94
|
+
rescue TimeoutError => ex
|
95
|
+
Qcmd.log "[error: reply timeout]"
|
96
|
+
# clear expecting reply item, assume it will never arrive
|
97
|
+
@sent_messages_expecting_reply.shift
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def send_command command, *args
|
102
|
+
options = args.extract_options!
|
103
|
+
|
104
|
+
Qcmd.debug "(building command from command, args, options: #{ command.inspect }, #{ args.inspect }, #{ options.inspect })"
|
105
|
+
|
106
|
+
# make sure command is valid OSC Address
|
107
|
+
if %r[^/] =~ command
|
108
|
+
address = command
|
109
|
+
else
|
110
|
+
address = "/#{ command }"
|
111
|
+
end
|
112
|
+
|
113
|
+
osc_message = OSC::Message.new address, *args
|
114
|
+
|
115
|
+
send_message osc_message
|
116
|
+
end
|
117
|
+
|
118
|
+
def send_message osc_message
|
119
|
+
Qcmd.debug "(sending osc message #{ osc_message.address } #{osc_message.has_arguments? ? 'with' : 'without'} args)"
|
120
|
+
|
121
|
+
@sent_messages << osc_message
|
122
|
+
if Qcmd::Commands.expects_reply?(osc_message)
|
123
|
+
Qcmd.debug "(this command expects a reply)"
|
124
|
+
@sent_messages_expecting_reply << osc_message
|
125
|
+
end
|
126
|
+
|
127
|
+
wait_for_replies do
|
128
|
+
send_channel.send osc_message
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def stop
|
133
|
+
Thread.kill(receive_thread) if receive_thread.alive?
|
134
|
+
end
|
135
|
+
|
136
|
+
def run
|
137
|
+
Qcmd.debug '(starting server)'
|
138
|
+
self.receive_thread = Thread.new do
|
139
|
+
Qcmd.debug '(server is up)'
|
140
|
+
receive_channel.run
|
141
|
+
end
|
142
|
+
end
|
143
|
+
alias :start :run
|
144
|
+
|
145
|
+
def send_workspace_command _command, *args
|
146
|
+
command = "workspace/#{ Qcmd.context.workspace.id }/#{ _command }"
|
147
|
+
send_command(command, *args)
|
148
|
+
end
|
149
|
+
|
150
|
+
def send_cue_command number, action, *args
|
151
|
+
command = "cue/#{ number }/#{ action }"
|
152
|
+
send_workspace_command(command, *args)
|
153
|
+
end
|
154
|
+
|
155
|
+
## QLab commands
|
156
|
+
|
157
|
+
def load_workspaces
|
158
|
+
send_command 'workspaces'
|
159
|
+
end
|
160
|
+
|
161
|
+
def load_cues
|
162
|
+
send_workspace_command 'cueLists'
|
163
|
+
end
|
164
|
+
|
165
|
+
def connect_to_workspace workspace
|
166
|
+
if workspace.passcode?
|
167
|
+
send_command "workspace/#{workspace.id}/connect", workspace.passcode
|
168
|
+
else
|
169
|
+
send_command "workspace/#{workspace.id}/connect"
|
170
|
+
end
|
171
|
+
|
172
|
+
# if it worked, load cues automatically
|
173
|
+
if Qcmd.context.workspace
|
174
|
+
load_cues
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/qcmd/version.rb
ADDED
data/qcmd.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'qcmd/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "qcmd"
|
8
|
+
gem.version = Qcmd::VERSION
|
9
|
+
gem.authors = ["Adam Bachman"]
|
10
|
+
gem.email = ["adam.bachman@gmail.com"]
|
11
|
+
gem.description = %q{A simple interactive QLab 3 command line controller}
|
12
|
+
gem.summary = %q{QLab 3 console}
|
13
|
+
gem.homepage = "https://github.com/abachman/qcmd"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_runtime_dependency 'dnssd'
|
21
|
+
gem.add_runtime_dependency 'eventmachine'
|
22
|
+
gem.add_runtime_dependency 'json'
|
23
|
+
gem.add_runtime_dependency 'osc-ruby'
|
24
|
+
gem.add_runtime_dependency 'trollop'
|
25
|
+
|
26
|
+
gem.add_development_dependency "rspec"
|
27
|
+
gem.add_development_dependency "cucumber"
|
28
|
+
gem.add_development_dependency "aruba"
|
29
|
+
gem.add_development_dependency 'ruby-debug'
|
30
|
+
end
|