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/parser.rb
CHANGED
@@ -1,94 +1,25 @@
|
|
1
|
+
# require 'sxp'
|
2
|
+
require 'vendor/sexpistol/sexpistol'
|
3
|
+
|
1
4
|
module Qcmd
|
2
5
|
module Parser
|
3
6
|
class << self
|
4
|
-
|
5
|
-
|
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
|
-
# A helper method to take care of the repetitive stuff for us
|
41
|
-
def is_match?( string, pattern)
|
42
|
-
match = string.match(pattern)
|
43
|
-
return false unless match
|
44
|
-
# Make sure that the matched pattern consumes the entire token
|
45
|
-
match[0].length == string.length
|
46
|
-
end
|
47
|
-
|
48
|
-
# Detect a symbol
|
49
|
-
def is_symbol?( string )
|
50
|
-
# Anything other than parentheses, single or double quote and commas
|
51
|
-
return is_match?( string, /[^\"\'\,\(\)]+/ )
|
52
|
-
end
|
53
|
-
|
54
|
-
# Detect an integer literal
|
55
|
-
def is_integer_literal?( string )
|
56
|
-
# Any number of numerals optionally preceded by a plus or minus sign
|
57
|
-
return is_match?( string, /[\-\+]?[0-9]+/ )
|
58
|
-
end
|
59
|
-
|
60
|
-
def is_float_literal?( string )
|
61
|
-
# Any number of numerals optionally preceded by a plus or minus sign
|
62
|
-
return is_match?( string, /[\-\+]?[0-9]+(\.[0-9]*)?/ )
|
63
|
-
end
|
64
|
-
|
65
|
-
# Detect a string literal
|
66
|
-
def is_string_literal?( string )
|
67
|
-
# Any characters except double quotes
|
68
|
-
# (except if preceded by a backslash), surrounded by quotes
|
69
|
-
return is_match?( string, /"([^"\\]|\\.)*"/)
|
7
|
+
def parser
|
8
|
+
@parser ||= Sexpistol.new
|
70
9
|
end
|
71
10
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
# If we haven't recognized the token by now we need to raise
|
80
|
-
# an exception as there are no more rules left to check against!
|
81
|
-
raise Exception, "Unrecognized token: #{t}"
|
11
|
+
def parse(string)
|
12
|
+
# make sure string is wrapped in parens to make the parser happy
|
13
|
+
begin
|
14
|
+
parser.parse_string "#{ string }"
|
15
|
+
rescue => ex
|
16
|
+
puts "parser FAILED WITH EXCEPTION: #{ ex.message }"
|
17
|
+
raise
|
82
18
|
end
|
83
|
-
return converted_tokens
|
84
19
|
end
|
85
20
|
|
86
|
-
def
|
87
|
-
|
88
|
-
token_array = tokenize_string(string)
|
89
|
-
token_array = restore_string_literals(token_array, string_literals)
|
90
|
-
token_array = convert_tokens(token_array)
|
91
|
-
return token_array
|
21
|
+
def generate(sexp)
|
22
|
+
parser.to_sexp(sexp)
|
92
23
|
end
|
93
24
|
end
|
94
25
|
end
|
data/lib/qcmd/plaintext.rb
CHANGED
data/lib/qcmd/qlab.rb
CHANGED
data/lib/qcmd/qlab/cue.rb
CHANGED
@@ -50,6 +50,14 @@ module Qcmd
|
|
50
50
|
self.data = options
|
51
51
|
end
|
52
52
|
|
53
|
+
def sync
|
54
|
+
Qcmd.debug "[Cue sync] synchronizing cue with id #{ self.id }"
|
55
|
+
|
56
|
+
# reload cue properties from QLab
|
57
|
+
fields = %w(uniqueID number name type colorName flagged armed cues)
|
58
|
+
self.data = Qcmd::CueAction.evaluate("cue_id #{ self.id } valuesForKeys #{ JSON.dump(fields).inspect }")
|
59
|
+
end
|
60
|
+
|
53
61
|
def id
|
54
62
|
data['uniqueID']
|
55
63
|
end
|
@@ -65,6 +73,18 @@ module Qcmd
|
|
65
73
|
def type
|
66
74
|
data['type']
|
67
75
|
end
|
76
|
+
|
77
|
+
def cues
|
78
|
+
if data['cues'].nil?
|
79
|
+
[]
|
80
|
+
else
|
81
|
+
data['cues'].map {|c| Qcmd::QLab::Cue.new(c)}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def has_cues?
|
86
|
+
cues.size > 0
|
87
|
+
end
|
68
88
|
end
|
69
89
|
end
|
70
90
|
end
|
@@ -0,0 +1,83 @@
|
|
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
|
+
#
|
17
|
+
# If the cue is a group, the dictionary will include an array of cue dictionaries for all children in the group:
|
18
|
+
#
|
19
|
+
# [
|
20
|
+
# {
|
21
|
+
# "uniqueID": string,
|
22
|
+
# "number": string
|
23
|
+
# "name": string
|
24
|
+
# "type": string
|
25
|
+
# "colorName": string
|
26
|
+
# "flagged": number
|
27
|
+
# "armed": number
|
28
|
+
# "cues": [ { }, { }, { } ]
|
29
|
+
# }
|
30
|
+
# ]
|
31
|
+
#
|
32
|
+
# [{\"number\":\"\",
|
33
|
+
# \"uniqueID\":\"1\",
|
34
|
+
# \"cues\":[{\"number\":\"1\",
|
35
|
+
# \"uniqueID\":\"2\",
|
36
|
+
# \"flagged\":false,
|
37
|
+
# \"type\":\"Wait\",
|
38
|
+
# \"colorName\":\"none\",
|
39
|
+
# \"name\":\"boom\",
|
40
|
+
# \"armed\":true}],
|
41
|
+
# \"flagged\":false,
|
42
|
+
# \"type\":\"Group\",
|
43
|
+
# \"colorName\":\"none\",
|
44
|
+
# \"name\":\"Main Cue List\",
|
45
|
+
# \"armed\":true}]
|
46
|
+
|
47
|
+
class CueList
|
48
|
+
attr_accessor :data
|
49
|
+
|
50
|
+
def initialize data
|
51
|
+
self.data = data
|
52
|
+
end
|
53
|
+
|
54
|
+
def id
|
55
|
+
data['uniqueID']
|
56
|
+
end
|
57
|
+
|
58
|
+
def name
|
59
|
+
data['listName']
|
60
|
+
end
|
61
|
+
|
62
|
+
def number
|
63
|
+
data['number']
|
64
|
+
end
|
65
|
+
|
66
|
+
def type
|
67
|
+
data['type']
|
68
|
+
end
|
69
|
+
|
70
|
+
def cues
|
71
|
+
if data['cues'].nil?
|
72
|
+
[]
|
73
|
+
else
|
74
|
+
data['cues'].map {|c| Qcmd::QLab::Cue.new(c)}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def has_cues?
|
79
|
+
cues.size > 0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/qcmd/qlab/reply.rb
CHANGED
@@ -2,7 +2,13 @@ module Qcmd
|
|
2
2
|
module QLab
|
3
3
|
class Reply < Struct.new(:osc_message)
|
4
4
|
def json
|
5
|
-
@json ||=
|
5
|
+
@json ||= begin
|
6
|
+
Qcmd.debug "[Reply json] parsing osc_message #{ osc_message.to_a.inspect }"
|
7
|
+
JSON.parse(osc_message.to_a.first)
|
8
|
+
rescue => ex
|
9
|
+
Qcmd.debug "[Reply json] json parsing of osc_message failed on message #{ osc_message.to_a.inspect }. #{ ex.message }"
|
10
|
+
{}
|
11
|
+
end
|
6
12
|
end
|
7
13
|
|
8
14
|
def address
|
@@ -13,12 +19,20 @@ module Qcmd
|
|
13
19
|
@data ||= json['data']
|
14
20
|
end
|
15
21
|
|
16
|
-
def
|
17
|
-
|
22
|
+
def has_data?
|
23
|
+
!data.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
def status
|
27
|
+
@status ||= json['status']
|
28
|
+
end
|
29
|
+
|
30
|
+
def empty?
|
31
|
+
false
|
18
32
|
end
|
19
33
|
|
20
34
|
def to_s
|
21
|
-
"<Qcmd::Qlab::Reply address:'#{address}' data:#{data.inspect}>"
|
35
|
+
"<Qcmd::Qlab::Reply address:'#{address}' status:'#{status}' data:#{data.inspect}>"
|
22
36
|
end
|
23
37
|
end
|
24
38
|
end
|
data/lib/qcmd/qlab/workspace.rb
CHANGED
@@ -6,7 +6,7 @@ module Qcmd
|
|
6
6
|
# "hasPasscode": number
|
7
7
|
#
|
8
8
|
class Workspace
|
9
|
-
attr_accessor :data, :passcode, :cue_lists
|
9
|
+
attr_accessor :data, :passcode, :cue_lists
|
10
10
|
|
11
11
|
def initialize options={}
|
12
12
|
self.data = options
|
@@ -23,6 +23,28 @@ module Qcmd
|
|
23
23
|
def id
|
24
24
|
data['uniqueID']
|
25
25
|
end
|
26
|
+
|
27
|
+
# all cues in this workspace
|
28
|
+
def cues
|
29
|
+
cue_lists.map do |cl|
|
30
|
+
load_cues(cl, [])
|
31
|
+
end.flatten.compact
|
32
|
+
end
|
33
|
+
|
34
|
+
def has_cues?
|
35
|
+
cues.size > 0
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def load_cues parent_cue, cues
|
41
|
+
parent_cue.cues.each {|child_cue|
|
42
|
+
cues << child_cue
|
43
|
+
load_cues child_cue, cues
|
44
|
+
}
|
45
|
+
|
46
|
+
cues
|
47
|
+
end
|
26
48
|
end
|
27
49
|
end
|
28
50
|
end
|
data/lib/qcmd/version.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Aaron Gough (http://thingsaaronmade.com/)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# This class contains our logic for parsing
|
2
|
+
# S-Expressions. They are turned into a
|
3
|
+
# native Ruby representation like:
|
4
|
+
# [:def, :something [:lambda, [:a], [:do_something]]]
|
5
|
+
class Sexpistol
|
6
|
+
|
7
|
+
attr_accessor :ruby_keyword_literals, :scheme_compatability
|
8
|
+
|
9
|
+
# Parse a string containing an S-Expression into a
|
10
|
+
# nested set of Ruby arrays
|
11
|
+
def parse_string(string)
|
12
|
+
tree = SexpistolParser.new(string).parse
|
13
|
+
return convert_ruby_keyword_literals(tree) if(@ruby_keyword_literals)
|
14
|
+
return tree
|
15
|
+
end
|
16
|
+
|
17
|
+
# Convert symbols corresponding to Ruby's keyword literals
|
18
|
+
# into their literal forms
|
19
|
+
def convert_ruby_keyword_literals(expression)
|
20
|
+
return recursive_map(expression) do |x|
|
21
|
+
case x
|
22
|
+
when :'nil' then nil
|
23
|
+
when :'true' then true
|
24
|
+
when :'false' then false
|
25
|
+
else x
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Convert nil, true and false into (), #t and #f for compatability
|
31
|
+
# with Scheme
|
32
|
+
def convert_scheme_literals(data)
|
33
|
+
return recursive_map(data) do |x|
|
34
|
+
case x
|
35
|
+
when nil then []
|
36
|
+
when true then :"#t"
|
37
|
+
when false then :"#f"
|
38
|
+
else x
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Convert a set of nested arrays back into an S-Expression
|
44
|
+
def to_sexp(data)
|
45
|
+
data = convert_scheme_literals(data) if(@scheme_compatability)
|
46
|
+
if( data.is_a?(Array))
|
47
|
+
mapped = data.map do |item|
|
48
|
+
if( item.is_a?(Array))
|
49
|
+
to_sexp(item)
|
50
|
+
else
|
51
|
+
item.to_s
|
52
|
+
end
|
53
|
+
end
|
54
|
+
"(" + mapped.join(" ") + ")"
|
55
|
+
else
|
56
|
+
data.to_s
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def recursive_map(data, &block)
|
63
|
+
if(data.is_a?(Array))
|
64
|
+
return data.map do |x|
|
65
|
+
if(x.is_a?(Array))
|
66
|
+
recursive_map(x, &block)
|
67
|
+
else
|
68
|
+
block.call(x)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
else
|
72
|
+
block.call(data)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
class SexpistolParser < StringScanner
|
4
|
+
|
5
|
+
def initialize(string)
|
6
|
+
# step through string counting closing parens, exclude parens in string literals
|
7
|
+
in_string_literal = false
|
8
|
+
escape_char = false
|
9
|
+
paren_count = 0
|
10
|
+
string.bytes.each do |byte|
|
11
|
+
if escape_char
|
12
|
+
escape_char = false
|
13
|
+
next
|
14
|
+
end
|
15
|
+
|
16
|
+
case byte.chr
|
17
|
+
when '\\'
|
18
|
+
escape_char = true
|
19
|
+
next
|
20
|
+
when '('
|
21
|
+
if !in_string_literal
|
22
|
+
paren_count += 1
|
23
|
+
end
|
24
|
+
when ')'
|
25
|
+
if !in_string_literal
|
26
|
+
paren_count -= 1
|
27
|
+
end
|
28
|
+
when '"'
|
29
|
+
in_string_literal = !in_string_literal
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if paren_count > 0
|
34
|
+
raise Exception, "Missing closing parentheses"
|
35
|
+
elsif paren_count < 0
|
36
|
+
raise Exception, "Missing opening parentheses"
|
37
|
+
end
|
38
|
+
|
39
|
+
super(string)
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse
|
43
|
+
exp = []
|
44
|
+
while true
|
45
|
+
case fetch_token
|
46
|
+
when '('
|
47
|
+
exp << parse
|
48
|
+
when ')'
|
49
|
+
break
|
50
|
+
when :"'"
|
51
|
+
case fetch_token
|
52
|
+
when '(' then exp << [:quote].concat([parse])
|
53
|
+
else exp << [:quote, @token]
|
54
|
+
end
|
55
|
+
when String, Fixnum, Float, Symbol
|
56
|
+
exp << @token
|
57
|
+
when nil
|
58
|
+
break
|
59
|
+
end
|
60
|
+
end
|
61
|
+
exp
|
62
|
+
end
|
63
|
+
|
64
|
+
def fetch_token
|
65
|
+
skip(/\s+/)
|
66
|
+
return nil if(eos?)
|
67
|
+
|
68
|
+
@token =
|
69
|
+
# Match parentheses
|
70
|
+
if scan(/[\(\)]/)
|
71
|
+
matched
|
72
|
+
# Match a string literal
|
73
|
+
elsif scan(/"([^"\\]|\\.)*"/)
|
74
|
+
eval(matched)
|
75
|
+
# Match a float literal
|
76
|
+
elsif scan(/[\-\+]? [0-9]+ ((e[0-9]+) | (\.[0-9]+(e[0-9]+)?))/x)
|
77
|
+
matched.to_f
|
78
|
+
# Match an integer literal
|
79
|
+
elsif scan(/[\-\+]?[0-9]+/)
|
80
|
+
matched.to_i
|
81
|
+
# Match a comma (for comma quoting)
|
82
|
+
elsif scan(/'/)
|
83
|
+
matched.to_sym
|
84
|
+
# Match a symbol
|
85
|
+
elsif scan(/[^\(\)\s]+/)
|
86
|
+
matched.to_sym
|
87
|
+
# If we've gotten here then we have an invalid token
|
88
|
+
else
|
89
|
+
near = scan %r{.{0,20}}
|
90
|
+
raise "Invalid character at position #{pos} near '#{near}'."
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|