qcmd 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- # 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
- # 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 convert_tokens( token_array )
73
- converted_tokens = []
74
- token_array.each do |t|
75
- converted_tokens << t.to_i and next if( is_integer_literal?(t) )
76
- converted_tokens << t.to_f and next if( is_float_literal?(t) )
77
- converted_tokens << t.to_s and next if( is_symbol?(t) )
78
- converted_tokens << eval(t) and next if( is_string_literal?(t) )
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 parse( string )
87
- string, string_literals = extract_string_literals(string)
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
@@ -75,10 +75,6 @@ module Qcmd
75
75
  }
76
76
  end
77
77
 
78
- def joined_wrapped_text line
79
- wrapped_text(line).join "\n"
80
- end
81
-
82
78
  # turn line into lines of text of columns length
83
79
  def wrapped_text *args
84
80
  options = {
@@ -1,3 +1,4 @@
1
1
  require 'qcmd/qlab/cue'
2
+ require 'qcmd/qlab/cue_list'
2
3
  require 'qcmd/qlab/reply'
3
4
  require 'qcmd/qlab/workspace'
@@ -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
@@ -2,7 +2,13 @@ module Qcmd
2
2
  module QLab
3
3
  class Reply < Struct.new(:osc_message)
4
4
  def json
5
- @json ||= JSON.parse(osc_message.to_a.first)
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 is_cue_command?
17
- Qcmd::Commands.is_cue_command?(address)
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
@@ -6,7 +6,7 @@ module Qcmd
6
6
  # "hasPasscode": number
7
7
  #
8
8
  class Workspace
9
- attr_accessor :data, :passcode, :cue_lists, :cues
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
@@ -1,3 +1,3 @@
1
1
  module Qcmd
2
- VERSION = "0.1.7"
2
+ VERSION = "0.1.8"
3
3
  end
@@ -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,2 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'sexpistol', 'sexpistol_parser.rb'))
2
+ require File.expand_path(File.join(File.dirname(__FILE__), 'sexpistol', 'sexpistol.rb'))
@@ -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