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.
@@ -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