qcmd 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,103 @@
1
+ module Qcmd
2
+ module Commands
3
+ # Commands that expect reponses
4
+ MACHINE_RESPONSE = %w(workspaces)
5
+
6
+ WORKSPACE_RESPONSE = %w(
7
+ cueLists selectedCues runningCues runningOrPausedCues connect thump
8
+ )
9
+
10
+ # commands that always expect a response
11
+ CUE_RESPONSE = %w(
12
+ uniqueID hasFileTargets hasCueTargets allowsEditingDuration isLoaded
13
+ isRunning isPaused isBroken preWaitElapsed actionElapsed
14
+ postWaitElapsed percentPreWaitElapsed percentActionElapsed
15
+ percentPostWaitElapsed type
16
+ basics children
17
+ )
18
+
19
+ # commands that expect a response if given without args
20
+ NO_ARG_CUE_RESPONSE = %w(
21
+ number name notes cueTargetNumber cueTargetId preWait duration
22
+ postWait continueMode flagged armed colorName
23
+ )
24
+
25
+ class << self
26
+ def machine_response_matcher
27
+ @machine_response_matcher ||= %r[(#{MACHINE_RESPONSE.join('|')})]
28
+ end
29
+ def machine_response_match command
30
+ !!(machine_response_matcher =~ command)
31
+ end
32
+
33
+ def workspace_response_matcher
34
+ @workspace_response_matcher ||= %r[(#{WORKSPACE_RESPONSE.join('|')})]
35
+ end
36
+ def workspace_response_match command
37
+ !!(workspace_response_matcher =~ command)
38
+ end
39
+
40
+ def cue_response_matcher
41
+ @cue_response_matcher ||= %r[(#{CUE_RESPONSE.join('|') })]
42
+ end
43
+ def cue_response_match command
44
+ !!(cue_response_matcher =~ command)
45
+ end
46
+
47
+ def cue_no_arg_response_matcher
48
+ @cue_no_arg_response_matcher ||= %r[(#{NO_ARG_CUE_RESPONSE.join('|') })]
49
+ end
50
+ def cue_no_arg_response_match command
51
+ !!(cue_no_arg_response_matcher =~ command)
52
+ end
53
+
54
+ def expects_reply? osc_message
55
+ address = osc_message.address
56
+
57
+ Qcmd.debug "(check #{address} for reply expectation in connection state #{Qcmd.context.connection_state})"
58
+
59
+ # debugger
60
+
61
+ case Qcmd.context.connection_state
62
+ when :none
63
+ # shouldn't be dealing with OSC messages when unconnected to
64
+ # machine or workspace
65
+ response = false
66
+ when :machine
67
+ # could be workspace or machine command
68
+ response = machine_response_match(address) ||
69
+ workspace_response_match(address)
70
+ when :workspace
71
+ if is_cue_command?(address)
72
+ Qcmd.debug "- (checking cue command)"
73
+ if osc_message.has_arguments?
74
+ Qcmd.debug "- (with arguments)"
75
+ response = cue_response_match address
76
+ else
77
+ Qcmd.debug "- (without arguments)"
78
+ response = cue_no_arg_response_match(address) ||
79
+ cue_response_match(address)
80
+ end
81
+ else
82
+ Qcmd.debug "- (checking workspace command)"
83
+ response = workspace_response_match(address) ||
84
+ machine_response_match(address)
85
+ end
86
+ end
87
+
88
+ response.tap {|value|
89
+ msg = value ? "EXPECT REPLY" : "do not expect reply"
90
+ Qcmd.debug "- (#{ msg })"
91
+ }
92
+ end
93
+
94
+ def is_cue_command? address
95
+ /cue/ =~ address && !(/Lists/ =~ address || /Cues/ =~ address)
96
+ end
97
+
98
+ def is_workspace_command? address
99
+ /workspace/ =~ address && !(%r[cue/] =~ address || %r[cue_id/] =~ address)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,37 @@
1
+ module Qcmd
2
+ class Context
3
+ attr_accessor :machine, :workspace, :workspace_connected
4
+
5
+ def reset
6
+ disconnect_machine
7
+ disconnect_workspace
8
+ end
9
+
10
+ def disconnect_machine
11
+ self.machine = nil
12
+ end
13
+
14
+ def disconnect_workspace
15
+ self.workspace = nil
16
+ self.workspace_connected = false
17
+ end
18
+
19
+ def machine_connected?
20
+ !machine.nil?
21
+ end
22
+
23
+ def workspace_connected?
24
+ !!workspace_connected
25
+ end
26
+
27
+ def connection_state
28
+ if !machine_connected?
29
+ :none
30
+ elsif !workspace_connected?
31
+ :machine
32
+ else
33
+ :workspace
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ # borrowed from ActiveSupport
2
+ class Array
3
+ def extract_options!
4
+ last.is_a?(Hash) ? pop : {}
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module OSC
2
+ class Message
3
+ def has_arguments?
4
+ to_a.size > 0
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,80 @@
1
+ module Qcmd
2
+ class Handler
3
+ include Qcmd::Plaintext
4
+
5
+ def handle reply
6
+ Qcmd.debug "(handling #{ reply })"
7
+
8
+ case reply.address
9
+ when %r[/workspaces]
10
+ Qcmd.context.machine.workspaces = reply.data.map {|ws| Qcmd::QLab::Workspace.new(ws)}
11
+
12
+ print centered_text(" Workspaces ", '-')
13
+ print
14
+ Qcmd.context.machine.workspaces.each_with_index do |ws, n|
15
+ print "#{ n + 1 }. #{ ws.name }"
16
+ end
17
+
18
+ print
19
+ print wrapped_text('Type `use "WORKSPACE_NAME" PASSCODE` to load a workspace. ' +
20
+ 'Only enter a passcode if your workspace uses one')
21
+ print
22
+
23
+ when %r[/workspace/[^/]+/connect]
24
+ # connecting to a workspace
25
+ if reply.data == 'badpass'
26
+ print 'failed to connect to workspace'
27
+ Qcmd.context.disconnect_workspace
28
+ elsif reply.data == 'ok'
29
+ print 'connected to workspace'
30
+ Qcmd.context.workspace_connected = true
31
+ end
32
+
33
+ when %r[/cueLists]
34
+ Qcmd.debug "(received cueLists)"
35
+
36
+ # load global cue list
37
+ Qcmd.context.workspace.cues = cues = reply.data.map {|cue_list|
38
+ cue_list['cues'].map {|cue| Qcmd::QLab::Cue.new(cue)}
39
+ }.compact.flatten
40
+
41
+ print "loaded #{pluralize cues.size, 'cue'}"
42
+
43
+ when %r[/(selectedCues|runningCues|runningOrPausedCues)]
44
+ cues = reply.data.map {|cue|
45
+ cues = [Qcmd::QLab::Cue.new(cue)]
46
+
47
+ if cue['cues']
48
+ cues << cue['cues'].map {|cue| Qcmd::QLab::Cue.new(cue)}
49
+ end
50
+
51
+ cues
52
+ }.compact.flatten
53
+
54
+ title = case reply.address
55
+ when /selectedCues/; "Selected Cues"
56
+ when /runningCues/; "Running Cues"
57
+ when /runningOrPausedCues/; "Running or Paused Cues"
58
+ end
59
+
60
+ print
61
+ print centered_text(" #{title} ", '-')
62
+ table(['Number', 'Id', 'Name', 'Type'], cues.map {|cue|
63
+ [cue.number, cue.id, cue.name, cue.type]
64
+ })
65
+ print
66
+
67
+ when %r[/(cue|cue_id)/[^/]+/[a-zA-Z]+]
68
+ # properties, just print reply data
69
+ result = reply.data
70
+ if result.is_a?(String) || result.is_a?(Numeric)
71
+ print result
72
+ else
73
+ print result.inspect
74
+ end
75
+ else
76
+ Qcmd.debug "(unrecognized message from QLab, cannot handle #{ reply.address })"
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,67 @@
1
+ require 'readline'
2
+
3
+ module Qcmd
4
+ module InputCompleter
5
+ ReservedWords = %w[
6
+ connect exit workspace workspaces disconnect
7
+ ]
8
+
9
+ ReservedWorkspaceWords = %w[
10
+ cueLists selectedCues runningCues runningOrPausedCues thump
11
+ ]
12
+
13
+ ReservedCueWords = %w[
14
+ cue stop pause resume load preview reset panic loadAt uniqueID
15
+ hasFileTargets hasCueTargets allowsEditingDuration isLoaded isRunning
16
+ isPaused isBroken preWaitElapsed actionElapsed postWaitElapsed
17
+ percentPreWaitElapsed percentActionElapsed percentPostWaitElapsed
18
+ type number name notes cueTargetNumber cueTargetId preWait duration
19
+ postWait continueMode flagged armed colorName basics children
20
+ ]
21
+
22
+ CompletionProc = Proc.new {|input|
23
+ # puts "input: #{ input }"
24
+
25
+ matcher = /^#{Regexp.escape(input)}/
26
+ commands = ReservedWords.grep(matcher)
27
+
28
+ if Qcmd.connected? && Qcmd.context
29
+ # have selected a machine
30
+ if Qcmd.context.workspace_connected?
31
+ # have selected a workspace
32
+ cue_numbers = Qcmd.context.workspace.cues.map(&:number)
33
+ commands = commands +
34
+ cue_numbers.grep(matcher) +
35
+ ReservedCueWords.grep(matcher) +
36
+ ReservedWorkspaceWords.grep(matcher)
37
+ else
38
+ # haven't selected a workspace yet
39
+ names = Qcmd.context.machine.workspace_names
40
+ quoted_names = names.map {|wn| %["#{wn}"]}
41
+ workspace_names = (names + quoted_names).grep(matcher)
42
+ workspace_names = workspace_names.map {|wsn|
43
+ if / / =~ wsn && /"/ !~ wsn
44
+ # if workspace name has a space and is not already quoted
45
+ %["#{ wsn }"]
46
+ else
47
+ wsn
48
+ end
49
+ }
50
+ commands = commands + workspace_names
51
+ end
52
+ else
53
+ # haven't selected a machine yet
54
+ commands = commands + Qcmd::Network.names.grep(matcher)
55
+ end
56
+
57
+ commands
58
+ }
59
+ end
60
+ end
61
+
62
+ if Readline.respond_to?("basic_word_break_characters=")
63
+ Readline.basic_word_break_characters= " \t\n`><=;|&{("
64
+ end
65
+ Readline.completion_append_character = nil
66
+ Readline.completion_case_fold = true
67
+ Readline.completion_proc = Qcmd::InputCompleter::CompletionProc
@@ -0,0 +1,27 @@
1
+ module Qcmd
2
+ class Machine < Struct.new(:name, :address, :port)
3
+ def client_arguments
4
+ [address, port]
5
+ end
6
+
7
+ def client_string
8
+ "#{ address }:#{ port }"
9
+ end
10
+
11
+ def workspaces= val
12
+ @workspaces = val
13
+ end
14
+
15
+ def workspaces
16
+ @workspaces || []
17
+ end
18
+
19
+ def workspace_names
20
+ @workspaces.map(&:name) || []
21
+ end
22
+
23
+ def find_workspace name
24
+ @workspaces.find {|ws| ws.name == name}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,70 @@
1
+ require 'dnssd'
2
+
3
+ module Qcmd
4
+ class Network
5
+ BROWSE_TIMEOUT = 2
6
+
7
+ class << self
8
+ attr_accessor :machines, :browse_thread
9
+
10
+ def browse
11
+ self.machines = []
12
+ self.browse_thread = Thread.start do
13
+ DNSSD.browse! '_qlab._udp' do |b|
14
+ DNSSD.resolve b.name, b.type, b.domain do |r|
15
+ self.machines << Qcmd::Machine.new(b.name, r.target, r.port)
16
+ end
17
+ end
18
+ end
19
+
20
+ naps = 0
21
+ changed = false
22
+ previous = 0
23
+
24
+ # sleep for 3 seconds
25
+ while naps < BROWSE_TIMEOUT
26
+ sleep 1
27
+ naps += 1
28
+
29
+ if machines.size != previous
30
+ Qcmd.print
31
+ Qcmd.print "Found #{ machines.size } QLab machine#{ machines.size == 1 ? '' : 's'}"
32
+ Qcmd.print
33
+ previous = machines.size
34
+ end
35
+ end
36
+
37
+ Thread.kill(browse_thread) if browse_thread.alive?
38
+ end
39
+
40
+ def display
41
+ longest = machines.map {|m| m.name.size}.max
42
+
43
+ machines.each_with_index do |machine, n|
44
+ if Qcmd.debug?
45
+ Qcmd.print "#{ n + 1 }. %-#{ longest + 2 }s %s" % [machine.name, machine.client_string]
46
+ else
47
+ Qcmd.print "#{ n + 1 }. %-#{ longest + 2 }s" % [machine.name]
48
+ end
49
+ end
50
+
51
+ Qcmd.print
52
+ Qcmd.print 'type `connect MACHINE` to connect to a machine'
53
+ Qcmd.print
54
+ end
55
+
56
+ def browse_and_display
57
+ browse
58
+ display
59
+ end
60
+
61
+ def find machine_name
62
+ machines.find {|m| m.name == machine_name}
63
+ end
64
+
65
+ def names
66
+ machines.map(&:name)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,48 @@
1
+ module Qcmd
2
+ module Parser
3
+ 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
+ def parse( string )
41
+ string, string_literals = extract_string_literals(string)
42
+ token_array = tokenize_string(string)
43
+ token_array = restore_string_literals(token_array, string_literals)
44
+ return token_array
45
+ end
46
+ end
47
+ end
48
+ end