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,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
|