qcmd 0.1.7 → 0.1.8
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 +7 -2
- data/TODO.md +31 -0
- data/bin/qcmd +2 -1
- data/lib/qcmd.rb +15 -3
- data/lib/qcmd/action.rb +160 -0
- data/lib/qcmd/aliases.rb +25 -0
- data/lib/qcmd/cli.rb +503 -108
- data/lib/qcmd/commands.rb +198 -142
- data/lib/qcmd/configuration.rb +83 -0
- data/lib/qcmd/context.rb +19 -13
- data/lib/qcmd/core_ext/osc/tcp_client.rb +155 -0
- data/lib/qcmd/handler.rb +49 -67
- data/lib/qcmd/history.rb +26 -0
- data/lib/qcmd/input_completer.rb +12 -2
- data/lib/qcmd/network.rb +9 -3
- data/lib/qcmd/parser.rb +14 -83
- data/lib/qcmd/plaintext.rb +0 -4
- data/lib/qcmd/qlab.rb +1 -0
- data/lib/qcmd/qlab/cue.rb +20 -0
- data/lib/qcmd/qlab/cue_list.rb +83 -0
- data/lib/qcmd/qlab/reply.rb +18 -4
- data/lib/qcmd/qlab/workspace.rb +23 -1
- data/lib/qcmd/version.rb +1 -1
- data/lib/vendor/sexpistol/LICENSE +20 -0
- data/lib/vendor/sexpistol/sexpistol.rb +2 -0
- data/lib/vendor/sexpistol/sexpistol/sexpistol.rb +76 -0
- data/lib/vendor/sexpistol/sexpistol/sexpistol_parser.rb +94 -0
- data/sample/dnssd.rb +20 -3
- data/sample/simple_console.rb +186 -43
- data/sample/tcp_qlab_connection.rb +67 -0
- data/spec/unit/action_spec.rb +84 -0
- data/spec/unit/commands_spec.rb +135 -14
- data/spec/unit/parser_spec.rb +36 -5
- metadata +124 -122
- data/lib/qcmd/core_ext/osc/stopping_server.rb +0 -84
- data/lib/qcmd/server.rb +0 -175
- data/spec/unit/osc_server_spec.rb +0 -78
data/lib/qcmd/context.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
module Qcmd
|
2
2
|
class Context
|
3
|
-
attr_accessor :machine, :workspace, :workspace_connected
|
3
|
+
attr_accessor :machine, :workspace, :workspace_connected, :cue, :cue_connected, :qlab
|
4
4
|
|
5
5
|
def reset
|
6
6
|
disconnect_machine
|
7
7
|
disconnect_workspace
|
8
|
+
disconnect_cue
|
8
9
|
end
|
9
10
|
|
10
11
|
def disconnect_machine
|
12
|
+
self.qlab.close unless self.qlab.nil?
|
11
13
|
self.machine = nil
|
12
14
|
end
|
13
15
|
|
@@ -16,6 +18,11 @@ module Qcmd
|
|
16
18
|
self.workspace_connected = false
|
17
19
|
end
|
18
20
|
|
21
|
+
def disconnect_cue
|
22
|
+
self.cue = nil
|
23
|
+
self.cue_connected = false
|
24
|
+
end
|
25
|
+
|
19
26
|
def machine_connected?
|
20
27
|
!machine.nil?
|
21
28
|
end
|
@@ -24,27 +31,26 @@ module Qcmd
|
|
24
31
|
!!workspace_connected
|
25
32
|
end
|
26
33
|
|
34
|
+
def cue_connected?
|
35
|
+
!!cue_connected
|
36
|
+
end
|
37
|
+
|
27
38
|
def connection_state
|
28
39
|
if !machine_connected?
|
29
40
|
:none
|
30
41
|
elsif !workspace_connected?
|
31
42
|
:machine
|
32
|
-
|
43
|
+
elsif !cue_connected?
|
33
44
|
:workspace
|
45
|
+
else
|
46
|
+
:cue
|
34
47
|
end
|
35
48
|
end
|
36
49
|
|
37
|
-
def
|
38
|
-
|
39
|
-
Qcmd
|
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
|
50
|
+
def connect_to_qlab handler=nil
|
51
|
+
# get an open connection with the default handler
|
52
|
+
handler ||= Qcmd::Handler
|
53
|
+
self.qlab = OSC::TCPClient.new(machine.address, machine.port, handler)
|
48
54
|
end
|
49
55
|
end
|
50
56
|
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# An OSC TCP client sends and receives on the same socket using the SLIP
|
2
|
+
# protocol.
|
3
|
+
#
|
4
|
+
# http://www.ietf.org/rfc/rfc1055.txt
|
5
|
+
|
6
|
+
module OSC
|
7
|
+
class TCPClient
|
8
|
+
|
9
|
+
CHAR_END = 0300 # indicates end of packet
|
10
|
+
CHAR_ESC = 0333 # indicates byte stuffing
|
11
|
+
CHAR_ESC_END = 0334 # ESC ESC_END means END data byte
|
12
|
+
CHAR_ESC_ESC = 0335 # ESC ESC_ESC means ESC data byte
|
13
|
+
|
14
|
+
CHAR_END_ENC = [0300].pack('C') # indicates end of packet
|
15
|
+
CHAR_ESC_ENC = [0333].pack('C') # indicates byte stuffing
|
16
|
+
CHAR_ESC_END_ENC = [0334].pack('C') # ESC ESC_END means END data byte
|
17
|
+
CHAR_ESC_ESC_ENC = [0335].pack('C') # ESC ESC_ESC means ESC data byte
|
18
|
+
|
19
|
+
def initialize host, port, handler=nil
|
20
|
+
@host = host
|
21
|
+
@port = port
|
22
|
+
@handler = handler
|
23
|
+
@so = TCPSocket.new host, port
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
@so.close unless closed?
|
28
|
+
end
|
29
|
+
|
30
|
+
def closed?
|
31
|
+
@so.closed?
|
32
|
+
end
|
33
|
+
|
34
|
+
# send an OSC::Message
|
35
|
+
def send msg
|
36
|
+
enc_msg = msg.encode
|
37
|
+
|
38
|
+
send_char CHAR_END
|
39
|
+
|
40
|
+
enc_msg.bytes.each do |b|
|
41
|
+
case b
|
42
|
+
when CHAR_END
|
43
|
+
send_char CHAR_ESC
|
44
|
+
send_char CHAR_ESC_END
|
45
|
+
when CHAR_ESC
|
46
|
+
send_char CHAR_ESC
|
47
|
+
send_char CHAR_ESC_ESC
|
48
|
+
else
|
49
|
+
send_char b
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
send_char CHAR_END
|
54
|
+
|
55
|
+
if block_given? || @handler
|
56
|
+
messages = response
|
57
|
+
if !messages.nil?
|
58
|
+
messages.each do |message|
|
59
|
+
if block_given?
|
60
|
+
yield message
|
61
|
+
else
|
62
|
+
@handler.handle message
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def response
|
70
|
+
if received_messages = receive_raw
|
71
|
+
received_messages.map do |message|
|
72
|
+
OSCPacket.messages_from_network(message)
|
73
|
+
end.flatten
|
74
|
+
else
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
"#<OSC::TCPClient:#{ object_id } @host:#{ @host.inspect }, @port:#{ @port.inspect }, @handler:#{ @handler.to_s }>"
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def send_char c
|
86
|
+
@so.send [c].pack('C'), 0
|
87
|
+
end
|
88
|
+
|
89
|
+
def receive_raw
|
90
|
+
received = 0
|
91
|
+
messages = []
|
92
|
+
buffer = []
|
93
|
+
failed = false
|
94
|
+
received_any = false
|
95
|
+
|
96
|
+
loop do
|
97
|
+
begin
|
98
|
+
# get a character from the socket, fail if nothing is available
|
99
|
+
c = @so.recv_nonblock(1)
|
100
|
+
|
101
|
+
received_any = true
|
102
|
+
|
103
|
+
case c
|
104
|
+
when CHAR_END_ENC
|
105
|
+
if received > 0
|
106
|
+
# add SLIP encoded message to list
|
107
|
+
messages << buffer.join
|
108
|
+
|
109
|
+
# reset state and keep reading from the port until there's
|
110
|
+
# nothing left
|
111
|
+
buffer.clear
|
112
|
+
received = 0
|
113
|
+
failed = false
|
114
|
+
end
|
115
|
+
when CHAR_ESC_ENC
|
116
|
+
# get next character, blocking is okay
|
117
|
+
c = @so.recv(1)
|
118
|
+
case c
|
119
|
+
when CHAR_ESC_END_ENC
|
120
|
+
c = CHAR_END_ENC
|
121
|
+
when CHAR_ESC_ESC_ENC
|
122
|
+
c = CHAR_ESC_ENC
|
123
|
+
else
|
124
|
+
received += 1
|
125
|
+
buffer << c
|
126
|
+
end
|
127
|
+
else
|
128
|
+
received += 1
|
129
|
+
buffer << c
|
130
|
+
end
|
131
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
132
|
+
# If any messages have been received, assume sender is done sending.
|
133
|
+
if failed || received_any
|
134
|
+
break
|
135
|
+
end
|
136
|
+
|
137
|
+
# wait one second to see if the socket might become readable (and a
|
138
|
+
# response forthcoming). normal usage is send + wait for response,
|
139
|
+
# we have to give QLab a reasonable amount of time in which to respond.
|
140
|
+
|
141
|
+
IO.select([@so], [], [], 1)
|
142
|
+
failed = true
|
143
|
+
retry
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
if messages.size > 0
|
148
|
+
messages
|
149
|
+
else
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
data/lib/qcmd/handler.rb
CHANGED
@@ -1,86 +1,68 @@
|
|
1
1
|
module Qcmd
|
2
2
|
class Handler
|
3
|
-
|
3
|
+
class << self
|
4
|
+
include Qcmd::Plaintext
|
4
5
|
|
5
|
-
|
6
|
-
|
6
|
+
# Handle OSC response message from QLab
|
7
|
+
def handle message
|
8
|
+
Qcmd.debug "[Handler handle] converting OSC::Message to QLab::Reply"
|
9
|
+
reply = QLab::Reply.new(message)
|
7
10
|
|
8
|
-
|
9
|
-
when %r[/workspaces]
|
10
|
-
Qcmd.context.machine.workspaces = reply.data.map {|ws| Qcmd::QLab::Workspace.new(ws)}
|
11
|
+
Qcmd.debug "[Handler handle] handling #{ reply.to_s }"
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
when %r[/workspace/[^/]+/connect]
|
17
|
-
# connecting to a workspace
|
18
|
-
if reply.data == 'badpass'
|
19
|
-
print 'failed to connect to workspace, bad passcode or no passcode given'
|
20
|
-
Qcmd.context.disconnect_workspace
|
21
|
-
elsif reply.data == 'ok'
|
22
|
-
print 'connected to workspace'
|
23
|
-
Qcmd.context.workspace_connected = true
|
24
|
-
end
|
25
|
-
|
26
|
-
when %r[/cueLists]
|
27
|
-
Qcmd.debug "(received cueLists)"
|
13
|
+
case reply.address
|
14
|
+
when %r[/(selectedCues|runningCues|runningOrPausedCues)]
|
15
|
+
Qcmd.debug "[Handler handle] received cue list from #{reply.address}"
|
28
16
|
|
29
|
-
|
30
|
-
|
31
|
-
cue_list['cues'].map {|cue| Qcmd::QLab::Cue.new(cue)}
|
32
|
-
}.compact.flatten
|
17
|
+
if reply.has_data?
|
18
|
+
cues = reply.data.map {|cue| Qcmd::QLab::Cue.new(cue)}
|
33
19
|
|
34
|
-
|
35
|
-
|
36
|
-
|
20
|
+
if cues.size > 0
|
21
|
+
title = case reply.address
|
22
|
+
when /selectedCues/; "Selected Cues"
|
23
|
+
when /runningCues/; "Running Cues"
|
24
|
+
when /runningOrPausedCues/; "Running or Paused Cues"
|
25
|
+
end
|
37
26
|
|
38
|
-
|
39
|
-
|
27
|
+
print
|
28
|
+
print centered_text(" #{title} ", '-')
|
29
|
+
table(['Number', 'Id', 'Name', 'Type'], cues.map {|cue|
|
30
|
+
[cue.number, cue.id, cue.name, cue.type]
|
31
|
+
})
|
32
|
+
print
|
33
|
+
else
|
34
|
+
print "no cues found"
|
35
|
+
end
|
40
36
|
end
|
41
37
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
title = case reply.address
|
46
|
-
when /selectedCues/; "Selected Cues"
|
47
|
-
when /runningCues/; "Running Cues"
|
48
|
-
when /runningOrPausedCues/; "Running or Paused Cues"
|
49
|
-
end
|
50
|
-
|
51
|
-
print
|
52
|
-
print centered_text(" #{title} ", '-')
|
53
|
-
table(['Number', 'Id', 'Name', 'Type'], cues.map {|cue|
|
54
|
-
[cue.number, cue.id, cue.name, cue.type]
|
55
|
-
})
|
56
|
-
print
|
38
|
+
when %r[/thump]
|
39
|
+
print reply.data
|
57
40
|
|
58
|
-
when %r[/(cue|cue_id)/[^/]+/[a-zA-Z]+]
|
59
|
-
# properties, just print reply data
|
60
|
-
result = reply.data
|
61
|
-
if result.is_a?(String) || result.is_a?(Numeric)
|
62
|
-
print result
|
63
41
|
else
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
42
|
+
Qcmd.debug "[Handler handle] unrecognized message from QLab, cannot handle #{ reply.address }"
|
43
|
+
|
44
|
+
if !reply.status.nil? && reply.status != 'ok'
|
45
|
+
print reply.status
|
76
46
|
end
|
77
47
|
end
|
48
|
+
end
|
78
49
|
|
79
|
-
|
80
|
-
|
50
|
+
def print_workspace_list
|
51
|
+
if Qcmd.context.machine.workspaces.nil? || Qcmd.context.machine.workspaces.empty?
|
52
|
+
Qcmd.print "there are no workspaces! you're gonna have a bad time :("
|
53
|
+
return
|
54
|
+
end
|
55
|
+
|
56
|
+
Qcmd.print Qcmd.centered_text(" Workspaces ", '-')
|
57
|
+
Qcmd.print
|
58
|
+
|
59
|
+
Qcmd.context.machine.workspaces.each_with_index do |ws, n|
|
60
|
+
Qcmd.print "#{ n + 1 }. #{ ws.name }#{ ws.passcode? ? ' [PROTECTED]' : ''}"
|
61
|
+
end
|
81
62
|
|
82
|
-
|
83
|
-
Qcmd.
|
63
|
+
Qcmd.print
|
64
|
+
Qcmd.print_wrapped('Type `use "WORKSPACE_NAME" PASSCODE` to load a workspace. Passcode is required if workspace is [PROTECTED].')
|
65
|
+
Qcmd.print
|
84
66
|
end
|
85
67
|
end
|
86
68
|
end
|
data/lib/qcmd/history.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Qcmd
|
2
|
+
class History
|
3
|
+
class << self
|
4
|
+
attr_accessor :commands
|
5
|
+
|
6
|
+
def load
|
7
|
+
if File.exists?(Qcmd::Configuration.history_file)
|
8
|
+
lines = File.new(Qcmd::Configuration.history_file, 'r').readlines
|
9
|
+
else
|
10
|
+
lines = []
|
11
|
+
end
|
12
|
+
|
13
|
+
if lines
|
14
|
+
lines.reverse[0..100].reverse.each {|hist|
|
15
|
+
Readline::HISTORY.push(hist)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def push command
|
22
|
+
Qcmd::Configuration.history.puts command
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/qcmd/input_completer.rb
CHANGED
@@ -7,9 +7,18 @@ module Qcmd
|
|
7
7
|
connect exit quit workspace workspaces disconnect help
|
8
8
|
]
|
9
9
|
|
10
|
-
ReservedWorkspaceWords = Qcmd::Commands::
|
10
|
+
ReservedWorkspaceWords = Qcmd::Commands::WORKSPACE
|
11
11
|
|
12
|
-
ReservedCueWords = Qcmd::Commands::
|
12
|
+
ReservedCueWords = Qcmd::Commands::ALL_CUES
|
13
|
+
|
14
|
+
def self.add_commands commands
|
15
|
+
ReservedWords.push(*commands)
|
16
|
+
ReservedWords.uniq!
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.add_command command
|
20
|
+
add_commands([command])
|
21
|
+
end
|
13
22
|
|
14
23
|
CompletionProc = Proc.new {|input|
|
15
24
|
# puts "input: #{ input }"
|
@@ -40,6 +49,7 @@ module Qcmd
|
|
40
49
|
quoted_names = machine_names.map {|mn| %["#{mn}"]}
|
41
50
|
names = (quoted_names + machine_names).grep(matcher)
|
42
51
|
names = quote_if_necessary(names)
|
52
|
+
|
43
53
|
# unquote
|
44
54
|
commands = commands + names
|
45
55
|
end
|
data/lib/qcmd/network.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'dnssd'
|
2
2
|
|
3
3
|
module Qcmd
|
4
|
+
# Browse the LAN and find open and running QLab instances.
|
4
5
|
class Network
|
5
6
|
BROWSE_TIMEOUT = 2
|
6
7
|
|
@@ -10,8 +11,9 @@ module Qcmd
|
|
10
11
|
# browse can be used alone to populate the machines list
|
11
12
|
def browse
|
12
13
|
self.machines = []
|
14
|
+
|
13
15
|
self.browse_thread = Thread.start do
|
14
|
-
DNSSD.browse! '_qlab.
|
16
|
+
DNSSD.browse! '_qlab._tcp.' do |b|
|
15
17
|
DNSSD.resolve b.name, b.type, b.domain do |r|
|
16
18
|
self.machines << Qcmd::Machine.new(b.name, r.target, r.port)
|
17
19
|
end
|
@@ -39,7 +41,7 @@ module Qcmd
|
|
39
41
|
end
|
40
42
|
|
41
43
|
Qcmd.print
|
42
|
-
Qcmd.print '
|
44
|
+
Qcmd.print 'Type `connect MACHINE` to connect to a machine'
|
43
45
|
Qcmd.print
|
44
46
|
end
|
45
47
|
|
@@ -51,7 +53,11 @@ module Qcmd
|
|
51
53
|
end
|
52
54
|
|
53
55
|
def find machine_name
|
54
|
-
machines.find {|m| m.name == machine_name}
|
56
|
+
machines.find {|m| m.name.to_s == machine_name.to_s}
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_by_index idx
|
60
|
+
machines[idx] if idx < machines.size
|
55
61
|
end
|
56
62
|
|
57
63
|
def names
|