qcmd 0.1.0

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