qcmd 0.1.7 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|