qcmd 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,103 @@
|
|
1
|
+
module Qcmd
|
2
|
+
module Commands
|
3
|
+
# Commands that expect reponses
|
4
|
+
MACHINE_RESPONSE = %w(workspaces)
|
5
|
+
|
6
|
+
WORKSPACE_RESPONSE = %w(
|
7
|
+
cueLists selectedCues runningCues runningOrPausedCues connect thump
|
8
|
+
)
|
9
|
+
|
10
|
+
# commands that always expect a response
|
11
|
+
CUE_RESPONSE = %w(
|
12
|
+
uniqueID hasFileTargets hasCueTargets allowsEditingDuration isLoaded
|
13
|
+
isRunning isPaused isBroken preWaitElapsed actionElapsed
|
14
|
+
postWaitElapsed percentPreWaitElapsed percentActionElapsed
|
15
|
+
percentPostWaitElapsed type
|
16
|
+
basics children
|
17
|
+
)
|
18
|
+
|
19
|
+
# commands that expect a response if given without args
|
20
|
+
NO_ARG_CUE_RESPONSE = %w(
|
21
|
+
number name notes cueTargetNumber cueTargetId preWait duration
|
22
|
+
postWait continueMode flagged armed colorName
|
23
|
+
)
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def machine_response_matcher
|
27
|
+
@machine_response_matcher ||= %r[(#{MACHINE_RESPONSE.join('|')})]
|
28
|
+
end
|
29
|
+
def machine_response_match command
|
30
|
+
!!(machine_response_matcher =~ command)
|
31
|
+
end
|
32
|
+
|
33
|
+
def workspace_response_matcher
|
34
|
+
@workspace_response_matcher ||= %r[(#{WORKSPACE_RESPONSE.join('|')})]
|
35
|
+
end
|
36
|
+
def workspace_response_match command
|
37
|
+
!!(workspace_response_matcher =~ command)
|
38
|
+
end
|
39
|
+
|
40
|
+
def cue_response_matcher
|
41
|
+
@cue_response_matcher ||= %r[(#{CUE_RESPONSE.join('|') })]
|
42
|
+
end
|
43
|
+
def cue_response_match command
|
44
|
+
!!(cue_response_matcher =~ command)
|
45
|
+
end
|
46
|
+
|
47
|
+
def cue_no_arg_response_matcher
|
48
|
+
@cue_no_arg_response_matcher ||= %r[(#{NO_ARG_CUE_RESPONSE.join('|') })]
|
49
|
+
end
|
50
|
+
def cue_no_arg_response_match command
|
51
|
+
!!(cue_no_arg_response_matcher =~ command)
|
52
|
+
end
|
53
|
+
|
54
|
+
def expects_reply? osc_message
|
55
|
+
address = osc_message.address
|
56
|
+
|
57
|
+
Qcmd.debug "(check #{address} for reply expectation in connection state #{Qcmd.context.connection_state})"
|
58
|
+
|
59
|
+
# debugger
|
60
|
+
|
61
|
+
case Qcmd.context.connection_state
|
62
|
+
when :none
|
63
|
+
# shouldn't be dealing with OSC messages when unconnected to
|
64
|
+
# machine or workspace
|
65
|
+
response = false
|
66
|
+
when :machine
|
67
|
+
# could be workspace or machine command
|
68
|
+
response = machine_response_match(address) ||
|
69
|
+
workspace_response_match(address)
|
70
|
+
when :workspace
|
71
|
+
if is_cue_command?(address)
|
72
|
+
Qcmd.debug "- (checking cue command)"
|
73
|
+
if osc_message.has_arguments?
|
74
|
+
Qcmd.debug "- (with arguments)"
|
75
|
+
response = cue_response_match address
|
76
|
+
else
|
77
|
+
Qcmd.debug "- (without arguments)"
|
78
|
+
response = cue_no_arg_response_match(address) ||
|
79
|
+
cue_response_match(address)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
Qcmd.debug "- (checking workspace command)"
|
83
|
+
response = workspace_response_match(address) ||
|
84
|
+
machine_response_match(address)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
response.tap {|value|
|
89
|
+
msg = value ? "EXPECT REPLY" : "do not expect reply"
|
90
|
+
Qcmd.debug "- (#{ msg })"
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def is_cue_command? address
|
95
|
+
/cue/ =~ address && !(/Lists/ =~ address || /Cues/ =~ address)
|
96
|
+
end
|
97
|
+
|
98
|
+
def is_workspace_command? address
|
99
|
+
/workspace/ =~ address && !(%r[cue/] =~ address || %r[cue_id/] =~ address)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/qcmd/context.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Qcmd
|
2
|
+
class Context
|
3
|
+
attr_accessor :machine, :workspace, :workspace_connected
|
4
|
+
|
5
|
+
def reset
|
6
|
+
disconnect_machine
|
7
|
+
disconnect_workspace
|
8
|
+
end
|
9
|
+
|
10
|
+
def disconnect_machine
|
11
|
+
self.machine = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def disconnect_workspace
|
15
|
+
self.workspace = nil
|
16
|
+
self.workspace_connected = false
|
17
|
+
end
|
18
|
+
|
19
|
+
def machine_connected?
|
20
|
+
!machine.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
def workspace_connected?
|
24
|
+
!!workspace_connected
|
25
|
+
end
|
26
|
+
|
27
|
+
def connection_state
|
28
|
+
if !machine_connected?
|
29
|
+
:none
|
30
|
+
elsif !workspace_connected?
|
31
|
+
:machine
|
32
|
+
else
|
33
|
+
:workspace
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/qcmd/handler.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Qcmd
|
2
|
+
class Handler
|
3
|
+
include Qcmd::Plaintext
|
4
|
+
|
5
|
+
def handle reply
|
6
|
+
Qcmd.debug "(handling #{ reply })"
|
7
|
+
|
8
|
+
case reply.address
|
9
|
+
when %r[/workspaces]
|
10
|
+
Qcmd.context.machine.workspaces = reply.data.map {|ws| Qcmd::QLab::Workspace.new(ws)}
|
11
|
+
|
12
|
+
print centered_text(" Workspaces ", '-')
|
13
|
+
print
|
14
|
+
Qcmd.context.machine.workspaces.each_with_index do |ws, n|
|
15
|
+
print "#{ n + 1 }. #{ ws.name }"
|
16
|
+
end
|
17
|
+
|
18
|
+
print
|
19
|
+
print wrapped_text('Type `use "WORKSPACE_NAME" PASSCODE` to load a workspace. ' +
|
20
|
+
'Only enter a passcode if your workspace uses one')
|
21
|
+
print
|
22
|
+
|
23
|
+
when %r[/workspace/[^/]+/connect]
|
24
|
+
# connecting to a workspace
|
25
|
+
if reply.data == 'badpass'
|
26
|
+
print 'failed to connect to workspace'
|
27
|
+
Qcmd.context.disconnect_workspace
|
28
|
+
elsif reply.data == 'ok'
|
29
|
+
print 'connected to workspace'
|
30
|
+
Qcmd.context.workspace_connected = true
|
31
|
+
end
|
32
|
+
|
33
|
+
when %r[/cueLists]
|
34
|
+
Qcmd.debug "(received cueLists)"
|
35
|
+
|
36
|
+
# load global cue list
|
37
|
+
Qcmd.context.workspace.cues = cues = reply.data.map {|cue_list|
|
38
|
+
cue_list['cues'].map {|cue| Qcmd::QLab::Cue.new(cue)}
|
39
|
+
}.compact.flatten
|
40
|
+
|
41
|
+
print "loaded #{pluralize cues.size, 'cue'}"
|
42
|
+
|
43
|
+
when %r[/(selectedCues|runningCues|runningOrPausedCues)]
|
44
|
+
cues = reply.data.map {|cue|
|
45
|
+
cues = [Qcmd::QLab::Cue.new(cue)]
|
46
|
+
|
47
|
+
if cue['cues']
|
48
|
+
cues << cue['cues'].map {|cue| Qcmd::QLab::Cue.new(cue)}
|
49
|
+
end
|
50
|
+
|
51
|
+
cues
|
52
|
+
}.compact.flatten
|
53
|
+
|
54
|
+
title = case reply.address
|
55
|
+
when /selectedCues/; "Selected Cues"
|
56
|
+
when /runningCues/; "Running Cues"
|
57
|
+
when /runningOrPausedCues/; "Running or Paused Cues"
|
58
|
+
end
|
59
|
+
|
60
|
+
print
|
61
|
+
print centered_text(" #{title} ", '-')
|
62
|
+
table(['Number', 'Id', 'Name', 'Type'], cues.map {|cue|
|
63
|
+
[cue.number, cue.id, cue.name, cue.type]
|
64
|
+
})
|
65
|
+
print
|
66
|
+
|
67
|
+
when %r[/(cue|cue_id)/[^/]+/[a-zA-Z]+]
|
68
|
+
# properties, just print reply data
|
69
|
+
result = reply.data
|
70
|
+
if result.is_a?(String) || result.is_a?(Numeric)
|
71
|
+
print result
|
72
|
+
else
|
73
|
+
print result.inspect
|
74
|
+
end
|
75
|
+
else
|
76
|
+
Qcmd.debug "(unrecognized message from QLab, cannot handle #{ reply.address })"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
module Qcmd
|
4
|
+
module InputCompleter
|
5
|
+
ReservedWords = %w[
|
6
|
+
connect exit workspace workspaces disconnect
|
7
|
+
]
|
8
|
+
|
9
|
+
ReservedWorkspaceWords = %w[
|
10
|
+
cueLists selectedCues runningCues runningOrPausedCues thump
|
11
|
+
]
|
12
|
+
|
13
|
+
ReservedCueWords = %w[
|
14
|
+
cue stop pause resume load preview reset panic loadAt uniqueID
|
15
|
+
hasFileTargets hasCueTargets allowsEditingDuration isLoaded isRunning
|
16
|
+
isPaused isBroken preWaitElapsed actionElapsed postWaitElapsed
|
17
|
+
percentPreWaitElapsed percentActionElapsed percentPostWaitElapsed
|
18
|
+
type number name notes cueTargetNumber cueTargetId preWait duration
|
19
|
+
postWait continueMode flagged armed colorName basics children
|
20
|
+
]
|
21
|
+
|
22
|
+
CompletionProc = Proc.new {|input|
|
23
|
+
# puts "input: #{ input }"
|
24
|
+
|
25
|
+
matcher = /^#{Regexp.escape(input)}/
|
26
|
+
commands = ReservedWords.grep(matcher)
|
27
|
+
|
28
|
+
if Qcmd.connected? && Qcmd.context
|
29
|
+
# have selected a machine
|
30
|
+
if Qcmd.context.workspace_connected?
|
31
|
+
# have selected a workspace
|
32
|
+
cue_numbers = Qcmd.context.workspace.cues.map(&:number)
|
33
|
+
commands = commands +
|
34
|
+
cue_numbers.grep(matcher) +
|
35
|
+
ReservedCueWords.grep(matcher) +
|
36
|
+
ReservedWorkspaceWords.grep(matcher)
|
37
|
+
else
|
38
|
+
# haven't selected a workspace yet
|
39
|
+
names = Qcmd.context.machine.workspace_names
|
40
|
+
quoted_names = names.map {|wn| %["#{wn}"]}
|
41
|
+
workspace_names = (names + quoted_names).grep(matcher)
|
42
|
+
workspace_names = workspace_names.map {|wsn|
|
43
|
+
if / / =~ wsn && /"/ !~ wsn
|
44
|
+
# if workspace name has a space and is not already quoted
|
45
|
+
%["#{ wsn }"]
|
46
|
+
else
|
47
|
+
wsn
|
48
|
+
end
|
49
|
+
}
|
50
|
+
commands = commands + workspace_names
|
51
|
+
end
|
52
|
+
else
|
53
|
+
# haven't selected a machine yet
|
54
|
+
commands = commands + Qcmd::Network.names.grep(matcher)
|
55
|
+
end
|
56
|
+
|
57
|
+
commands
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if Readline.respond_to?("basic_word_break_characters=")
|
63
|
+
Readline.basic_word_break_characters= " \t\n`><=;|&{("
|
64
|
+
end
|
65
|
+
Readline.completion_append_character = nil
|
66
|
+
Readline.completion_case_fold = true
|
67
|
+
Readline.completion_proc = Qcmd::InputCompleter::CompletionProc
|
data/lib/qcmd/machine.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Qcmd
|
2
|
+
class Machine < Struct.new(:name, :address, :port)
|
3
|
+
def client_arguments
|
4
|
+
[address, port]
|
5
|
+
end
|
6
|
+
|
7
|
+
def client_string
|
8
|
+
"#{ address }:#{ port }"
|
9
|
+
end
|
10
|
+
|
11
|
+
def workspaces= val
|
12
|
+
@workspaces = val
|
13
|
+
end
|
14
|
+
|
15
|
+
def workspaces
|
16
|
+
@workspaces || []
|
17
|
+
end
|
18
|
+
|
19
|
+
def workspace_names
|
20
|
+
@workspaces.map(&:name) || []
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_workspace name
|
24
|
+
@workspaces.find {|ws| ws.name == name}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/qcmd/network.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'dnssd'
|
2
|
+
|
3
|
+
module Qcmd
|
4
|
+
class Network
|
5
|
+
BROWSE_TIMEOUT = 2
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :machines, :browse_thread
|
9
|
+
|
10
|
+
def browse
|
11
|
+
self.machines = []
|
12
|
+
self.browse_thread = Thread.start do
|
13
|
+
DNSSD.browse! '_qlab._udp' do |b|
|
14
|
+
DNSSD.resolve b.name, b.type, b.domain do |r|
|
15
|
+
self.machines << Qcmd::Machine.new(b.name, r.target, r.port)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
naps = 0
|
21
|
+
changed = false
|
22
|
+
previous = 0
|
23
|
+
|
24
|
+
# sleep for 3 seconds
|
25
|
+
while naps < BROWSE_TIMEOUT
|
26
|
+
sleep 1
|
27
|
+
naps += 1
|
28
|
+
|
29
|
+
if machines.size != previous
|
30
|
+
Qcmd.print
|
31
|
+
Qcmd.print "Found #{ machines.size } QLab machine#{ machines.size == 1 ? '' : 's'}"
|
32
|
+
Qcmd.print
|
33
|
+
previous = machines.size
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Thread.kill(browse_thread) if browse_thread.alive?
|
38
|
+
end
|
39
|
+
|
40
|
+
def display
|
41
|
+
longest = machines.map {|m| m.name.size}.max
|
42
|
+
|
43
|
+
machines.each_with_index do |machine, n|
|
44
|
+
if Qcmd.debug?
|
45
|
+
Qcmd.print "#{ n + 1 }. %-#{ longest + 2 }s %s" % [machine.name, machine.client_string]
|
46
|
+
else
|
47
|
+
Qcmd.print "#{ n + 1 }. %-#{ longest + 2 }s" % [machine.name]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
Qcmd.print
|
52
|
+
Qcmd.print 'type `connect MACHINE` to connect to a machine'
|
53
|
+
Qcmd.print
|
54
|
+
end
|
55
|
+
|
56
|
+
def browse_and_display
|
57
|
+
browse
|
58
|
+
display
|
59
|
+
end
|
60
|
+
|
61
|
+
def find machine_name
|
62
|
+
machines.find {|m| m.name == machine_name}
|
63
|
+
end
|
64
|
+
|
65
|
+
def names
|
66
|
+
machines.map(&:name)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/qcmd/parser.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Qcmd
|
2
|
+
module Parser
|
3
|
+
class << self
|
4
|
+
# adapted from https://gist.github.com/612311
|
5
|
+
# by Aaron Gough
|
6
|
+
def extract_string_literals( string )
|
7
|
+
string_literal_pattern = /"([^"\\]|\\.)*"/
|
8
|
+
string_replacement_token = "___+++STRING_LITERAL+++___"
|
9
|
+
# Find and extract all the string literals
|
10
|
+
string_literals = []
|
11
|
+
string.gsub(string_literal_pattern) {|x| string_literals << x}
|
12
|
+
# Replace all the string literals with our special placeholder token
|
13
|
+
string = string.gsub(string_literal_pattern, string_replacement_token)
|
14
|
+
# Return the modified string and the array of string literals
|
15
|
+
return [string, string_literals]
|
16
|
+
end
|
17
|
+
|
18
|
+
def tokenize_string( string )
|
19
|
+
string = string.gsub("(", " ( ")
|
20
|
+
string = string.gsub(")", " ) ")
|
21
|
+
token_array = string.split(" ")
|
22
|
+
return token_array
|
23
|
+
end
|
24
|
+
|
25
|
+
def restore_string_literals( token_array, string_literals )
|
26
|
+
return token_array.map do |x|
|
27
|
+
if(x == '___+++STRING_LITERAL+++___')
|
28
|
+
# Since we've detected that a string literal needs to be
|
29
|
+
# replaced we will grab the first available string from the
|
30
|
+
# string_literals array
|
31
|
+
string_literals.shift
|
32
|
+
else
|
33
|
+
# This is not a string literal so we need to just return the
|
34
|
+
# token as it is
|
35
|
+
x
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse( string )
|
41
|
+
string, string_literals = extract_string_literals(string)
|
42
|
+
token_array = tokenize_string(string)
|
43
|
+
token_array = restore_string_literals(token_array, string_literals)
|
44
|
+
return token_array
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|