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.
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'qcmd/qlab/cue'
2
+ require 'qcmd/qlab/reply'
3
+ require 'qcmd/qlab/workspace'
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Qcmd
2
+ VERSION = "0.1.0"
3
+ end
@@ -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