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.
- 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/commands.rb
CHANGED
@@ -7,138 +7,143 @@ module Qcmd
|
|
7
7
|
# *_RESPONSE lists are commands that expect responses
|
8
8
|
#
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
cueLists selectedCues runningCues runningOrPausedCues thump
|
10
|
+
MACHINE = %w(
|
11
|
+
alwaysReply
|
12
|
+
connect
|
13
|
+
workingDirectory
|
14
|
+
workspaces
|
16
15
|
)
|
17
16
|
|
18
|
-
|
19
|
-
|
17
|
+
WORKSPACE = %w(
|
18
|
+
cueLists
|
19
|
+
go
|
20
|
+
hardStop
|
21
|
+
new
|
22
|
+
panic
|
23
|
+
pause
|
24
|
+
reset
|
25
|
+
resume
|
26
|
+
runningCues
|
27
|
+
runningOrPausedCues
|
28
|
+
select
|
29
|
+
selectedCues
|
30
|
+
stop
|
31
|
+
thump
|
32
|
+
toggleFullScreen
|
33
|
+
updates
|
20
34
|
)
|
21
35
|
|
22
|
-
ALL_WORKSPACE_COMMANDS = [WORKSPACE_RESPONSE + WORKSPACE_NO_RESPONSE]
|
23
|
-
|
24
36
|
# commands that take no args and do not respond
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
37
|
+
CUE = %w(
|
38
|
+
actionElapsed
|
39
|
+
allowsEditingDuration
|
40
|
+
armed
|
41
|
+
children
|
42
|
+
colorName
|
43
|
+
continueMode
|
44
|
+
cueTargetId
|
45
|
+
cueTargetNumber
|
46
|
+
defaultName
|
47
|
+
displayName
|
48
|
+
duration
|
49
|
+
fileTarget
|
50
|
+
flagged
|
51
|
+
hardStop
|
52
|
+
hasCueTargets
|
53
|
+
hasFileTargets
|
54
|
+
isBroken
|
55
|
+
isLoaded
|
56
|
+
isPaused
|
57
|
+
isRunning
|
58
|
+
listName
|
59
|
+
load
|
31
60
|
loadAt
|
61
|
+
name
|
62
|
+
notes
|
63
|
+
number
|
64
|
+
panic
|
65
|
+
pause
|
66
|
+
percentActionElapsed
|
67
|
+
percentPostWaitElapsed
|
68
|
+
percentPreWaitElapsed
|
69
|
+
postWait
|
70
|
+
postWaitElapsed
|
71
|
+
preWait
|
72
|
+
preWaitElapsed
|
73
|
+
preview
|
74
|
+
reset
|
75
|
+
resume
|
76
|
+
sliderLevel
|
77
|
+
sliderLevels
|
78
|
+
start
|
79
|
+
stop
|
80
|
+
togglePause
|
81
|
+
type
|
82
|
+
uniqueID
|
83
|
+
valuesForKeys
|
32
84
|
)
|
33
85
|
|
34
|
-
|
35
|
-
|
36
|
-
uniqueID hasFileTargets hasCueTargets allowsEditingDuration isLoaded
|
37
|
-
isRunning isPaused isBroken preWaitElapsed actionElapsed
|
38
|
-
postWaitElapsed percentPreWaitElapsed percentActionElapsed
|
39
|
-
percentPostWaitElapsed type sliderLevels
|
40
|
-
basics children
|
86
|
+
GROUP_CUE = %w(
|
87
|
+
playbackPositionId
|
41
88
|
)
|
42
89
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
90
|
+
AUDIO_CUE = %w(
|
91
|
+
doFade
|
92
|
+
doPitchShift
|
93
|
+
endTime
|
94
|
+
infiniteLoop
|
95
|
+
level
|
96
|
+
patch
|
97
|
+
playCount
|
98
|
+
rate
|
47
99
|
sliderLevel
|
100
|
+
sliderLevels
|
101
|
+
startTime
|
48
102
|
)
|
49
103
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
CUE_RESPONSE +
|
56
|
-
NO_ARG_CUE_RESPONSE
|
57
|
-
|
58
|
-
class << self
|
59
|
-
def no_machine_response_matcher
|
60
|
-
@no_machine_response_matcher ||= %r[(#{NO_MACHINE_RESPONSE.join('|')})]
|
61
|
-
end
|
62
|
-
def no_machine_response_match command
|
63
|
-
!!(no_machine_response_matcher =~ command)
|
64
|
-
end
|
65
|
-
|
66
|
-
def machine_response_matcher
|
67
|
-
@machine_response_matcher ||= %r[(#{MACHINE_RESPONSE.join('|')})]
|
68
|
-
end
|
69
|
-
def machine_response_match command
|
70
|
-
!!(machine_response_matcher =~ command)
|
71
|
-
end
|
72
|
-
|
73
|
-
def workspace_response_matcher
|
74
|
-
@workspace_response_matcher ||= %r[(#{WORKSPACE_RESPONSE.join('|')})]
|
75
|
-
end
|
76
|
-
def workspace_response_match command
|
77
|
-
!!(workspace_response_matcher =~ command)
|
78
|
-
end
|
79
|
-
|
80
|
-
def cue_response_matcher
|
81
|
-
@cue_response_matcher ||= %r[(#{CUE_RESPONSE.join('|') })]
|
82
|
-
end
|
83
|
-
def cue_response_match command
|
84
|
-
!!(cue_response_matcher =~ command)
|
85
|
-
end
|
86
|
-
|
87
|
-
def cue_no_arg_response_matcher
|
88
|
-
@cue_no_arg_response_matcher ||= %r[(#{NO_ARG_CUE_RESPONSE.join('|') })]
|
89
|
-
end
|
90
|
-
def cue_no_arg_response_match command
|
91
|
-
!!(cue_no_arg_response_matcher =~ command)
|
92
|
-
end
|
93
|
-
|
94
|
-
def expects_reply? osc_message
|
95
|
-
address = osc_message.address
|
96
|
-
|
97
|
-
Qcmd.debug "(check #{address} for reply expectation in connection state #{Qcmd.context.connection_state})"
|
98
|
-
|
99
|
-
# debugger
|
100
|
-
|
101
|
-
case Qcmd.context.connection_state
|
102
|
-
when :none
|
103
|
-
# shouldn't be dealing with OSC messages when unconnected to
|
104
|
-
# machine or workspace
|
105
|
-
response = no_machine_response_match(address)
|
106
|
-
when :machine
|
107
|
-
# could be workspace or machine command
|
108
|
-
response = machine_response_match(address) ||
|
109
|
-
workspace_response_match(address)
|
110
|
-
when :workspace
|
111
|
-
if is_cue_command?(address)
|
112
|
-
Qcmd.debug "- (checking cue command)"
|
113
|
-
if osc_message.has_arguments?
|
114
|
-
Qcmd.debug "- (with arguments)"
|
115
|
-
response = cue_response_match address
|
116
|
-
else
|
117
|
-
Qcmd.debug "- (without arguments)"
|
118
|
-
response = cue_no_arg_response_match(address) ||
|
119
|
-
cue_response_match(address)
|
120
|
-
end
|
121
|
-
else
|
122
|
-
Qcmd.debug "- (checking workspace command)"
|
123
|
-
response = workspace_response_match(address) ||
|
124
|
-
machine_response_match(address)
|
125
|
-
end
|
126
|
-
end
|
104
|
+
FADE_CUE = %w(
|
105
|
+
level
|
106
|
+
sliderLevel
|
107
|
+
sliderLevels
|
108
|
+
)
|
127
109
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
110
|
+
MIC_CUE = %w(
|
111
|
+
level
|
112
|
+
sliderLevel
|
113
|
+
sliderLevels
|
114
|
+
)
|
133
115
|
|
134
|
-
|
135
|
-
|
136
|
-
|
116
|
+
VIDEO_CUE = %w(
|
117
|
+
cueSize
|
118
|
+
doEffect
|
119
|
+
doFade
|
120
|
+
doPitchShift
|
121
|
+
effect
|
122
|
+
effectSet
|
123
|
+
endTime
|
124
|
+
fullScreen
|
125
|
+
infiniteLoop
|
126
|
+
layer
|
127
|
+
level
|
128
|
+
opacity
|
129
|
+
patch
|
130
|
+
playCount
|
131
|
+
preserveAspectRatio
|
132
|
+
quaternion
|
133
|
+
rate
|
134
|
+
scaleX
|
135
|
+
scaleY
|
136
|
+
sliderLevel
|
137
|
+
sliderLevels
|
138
|
+
startTime
|
139
|
+
surfaceID
|
140
|
+
surfaceList
|
141
|
+
surfaceSize
|
142
|
+
translationX
|
143
|
+
translationY
|
144
|
+
)
|
137
145
|
|
138
|
-
|
139
|
-
/workspace/ =~ address && !(%r[cue/] =~ address || %r[cue_id/] =~ address)
|
140
|
-
end
|
141
|
-
end
|
146
|
+
ALL_CUES = (CUE + GROUP_CUE + AUDIO_CUE + FADE_CUE + MIC_CUE + VIDEO_CUE).uniq.sort
|
142
147
|
|
143
148
|
module Help
|
144
149
|
class << self
|
@@ -147,43 +152,49 @@ module Qcmd
|
|
147
152
|
Qcmd.print %[
|
148
153
|
#{Qcmd.centered_text(' Available Commands ', '-')}
|
149
154
|
|
155
|
+
|
150
156
|
exit
|
151
157
|
|
152
|
-
|
158
|
+
Close qcmd.
|
153
159
|
|
154
160
|
|
155
|
-
connect
|
161
|
+
connect MACHINE_ID
|
156
162
|
|
157
|
-
|
163
|
+
Connect to the machine with id MACHINE_ID. This can either be the name of the
|
164
|
+
machine shown in the listing or its number on the list. Once a machine is
|
165
|
+
connected its name will appear above the prompt.
|
158
166
|
|
159
167
|
|
160
168
|
disconnect
|
161
169
|
|
162
|
-
|
170
|
+
Disconnect from the current machine and workspace.
|
171
|
+
|
172
|
+
|
173
|
+
..
|
174
|
+
|
175
|
+
Disconnect cue if one is connected. If not, disconnect the current workspace.
|
176
|
+
If one is not connected, disconnect from the machine.
|
163
177
|
|
164
178
|
|
165
179
|
use WORKSPACE_NAME [PASSCODE]
|
166
180
|
|
167
|
-
|
168
|
-
passcode is only required if the workspace
|
181
|
+
Connect to the workspace with name WORKSPACE_NAME given as a double quoted
|
182
|
+
string using passcode PASSCODE. A passcode is only required if the workspace
|
183
|
+
has one enabled. Once a workspace is connected its name will appear above the
|
184
|
+
prompt.
|
169
185
|
|
170
186
|
|
171
187
|
workspaces
|
172
188
|
|
173
|
-
|
189
|
+
Show a list of the available workspaces for the currently connected machine.
|
174
190
|
|
175
191
|
|
176
192
|
workspace COMMAND [VALUE]
|
177
193
|
|
178
|
-
|
179
|
-
act on a workspace
|
194
|
+
Pass the given COMMAND to the connected workspace. The following commands will
|
195
|
+
act on a workspace:
|
180
196
|
|
181
|
-
#{Qcmd.wrapped_text(Qcmd::Commands::
|
182
|
-
|
183
|
-
And these commands will not act on a workspace, but will return a value
|
184
|
-
(usually a list of cues):
|
185
|
-
|
186
|
-
#{Qcmd.wrapped_text((Qcmd::Commands::WORKSPACE_RESPONSE - ['connect']).sort.join(', '), :indent => ' ').join("\n")}
|
197
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::WORKSPACE.sort.join(', '), :indent => ' ').join("\n")}
|
187
198
|
|
188
199
|
* Pro Tip: once you are connected to a workspace, you can just use COMMAND
|
189
200
|
and leave off "workspace" to quickly send the given COMMAND to the connected
|
@@ -192,27 +203,72 @@ workspace COMMAND [VALUE]
|
|
192
203
|
|
193
204
|
cue NUMBER COMMAND [VALUE [ANOTHER_VALUE ...]]
|
194
205
|
|
195
|
-
|
206
|
+
or
|
207
|
+
|
208
|
+
cue_id ID COMMAND [VALUE [ANOTHER_VALUE ...]]
|
209
|
+
|
210
|
+
Send a command to the cue with the given NUMBER or ID depending on the way
|
211
|
+
you are addressing the cue.
|
196
212
|
|
197
|
-
NUMBER can be a string or a number, depending on the command.
|
213
|
+
NUMBER can be a double quoted string or a number, depending on the command.
|
198
214
|
|
199
215
|
COMMAND can be one of:
|
200
216
|
|
201
|
-
#{Qcmd.wrapped_text(Qcmd::Commands::
|
217
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::CUE.sort.join(', '), :indent => ' ').join("\n")}
|
218
|
+
|
219
|
+
Specific types of cues may have different cues available. Here's the commands
|
220
|
+
available for different types of cues:
|
221
|
+
|
222
|
+
Group Cue:
|
223
|
+
|
224
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::GROUP_CUE.sort.join(', '), :indent => ' ').join("\n")}
|
225
|
+
|
226
|
+
Audio Cue:
|
227
|
+
|
228
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::AUDIO_CUE.sort.join(', '), :indent => ' ').join("\n")}
|
229
|
+
|
230
|
+
Fade Cue:
|
231
|
+
|
232
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::FADE_CUE.sort.join(', '), :indent => ' ').join("\n")}
|
233
|
+
|
234
|
+
Mic Cue:
|
235
|
+
|
236
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::MIC_CUE.sort.join(', '), :indent => ' ').join("\n")}
|
237
|
+
|
238
|
+
Video Cue:
|
239
|
+
|
240
|
+
#{Qcmd.wrapped_text(Qcmd::Commands::VIDEO_CUE.sort.join(', '), :indent => ' ').join("\n")}
|
241
|
+
|
242
|
+
Once a command has been sent to an existing cue, subsequent cue commands will
|
243
|
+
be sent to the same cue with needing to repeat the leading "cue NUMBER". Once
|
244
|
+
a cue is connected its name will appear above the prompt.
|
245
|
+
|
246
|
+
|
247
|
+
alias COMMAND ACTION
|
248
|
+
|
249
|
+
Create a new command to use in qcmd! COMMAND should be a single word,
|
250
|
+
starting with made of one or more letters, numbers, underscores, and/or
|
251
|
+
hyphens. ACTION should be a legit qcmd program surrounded by parentheses,
|
252
|
+
which is anything you can type into qcmd. If you want your command to accept
|
253
|
+
arguments, use $1, $2, ... $n in place of the argument.
|
254
|
+
|
255
|
+
For example:
|
256
|
+
|
257
|
+
> alias cue-rename (cue $1 name "Hello $2")
|
258
|
+
|
259
|
+
Would create a new command, "cue-rename", that you could use to rename a cue:
|
202
260
|
|
203
|
-
|
204
|
-
a value, will update the cue:
|
261
|
+
> cue-rename 10 World
|
205
262
|
|
206
|
-
|
263
|
+
to rename cue number 10 to "Hello World".
|
207
264
|
|
208
|
-
|
265
|
+
We've included a few custom commands so you can see how it works. Aliases are
|
266
|
+
stored in ~/.qcmd/settings.json and can be edited from there.
|
209
267
|
|
210
|
-
#{Qcmd.wrapped_text(Qcmd::Commands::CUE_RESPONSE.sort.join(', '), :indent => ' ').join("\n")}
|
211
268
|
|
212
|
-
|
213
|
-
respond:
|
269
|
+
aliases
|
214
270
|
|
215
|
-
|
271
|
+
See all the aliases.
|
216
272
|
|
217
273
|
]
|
218
274
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Qcmd
|
4
|
+
class Configuration
|
5
|
+
class << self
|
6
|
+
def qcmd_directory
|
7
|
+
".qcmd"
|
8
|
+
end
|
9
|
+
|
10
|
+
def home_directory
|
11
|
+
@home_directory ||= begin
|
12
|
+
full_path = File.join(File.expand_path('~'), qcmd_directory)
|
13
|
+
begin
|
14
|
+
if !File.exists?(full_path)
|
15
|
+
FileUtils.mkdir_p(full_path)
|
16
|
+
end
|
17
|
+
full_path
|
18
|
+
rescue => ex
|
19
|
+
puts "Failed to create qcmd's home directory at #{ full_path }"
|
20
|
+
puts ex.message
|
21
|
+
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def open_file_for_appending(fname)
|
28
|
+
f = File.new(fname, 'a')
|
29
|
+
f.sync = true
|
30
|
+
f
|
31
|
+
end
|
32
|
+
|
33
|
+
def config
|
34
|
+
@config ||= begin
|
35
|
+
if !File.exists?(config_file)
|
36
|
+
File.open(config_file, 'w') {|f|
|
37
|
+
default = JSON.pretty_generate({'aliases' => Qcmd::Aliases.defaults})
|
38
|
+
Qcmd.debug "[Configuration config] writing defaults: #{ default }"
|
39
|
+
f.write default
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
JSON.load(File.open(config_file))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def update key, value
|
48
|
+
config[key] = value
|
49
|
+
save
|
50
|
+
end
|
51
|
+
|
52
|
+
def save
|
53
|
+
File.open(config_file, 'w') {|conf_file|
|
54
|
+
conf_file.write(JSON.pretty_generate(config))
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# not really config file things, but related to config & settings storage
|
59
|
+
|
60
|
+
def history
|
61
|
+
@history ||= open_file_for_appending(history_file)
|
62
|
+
end
|
63
|
+
|
64
|
+
def log
|
65
|
+
@log ||= open_file_for_appending(log_file)
|
66
|
+
end
|
67
|
+
|
68
|
+
# and the actual files
|
69
|
+
|
70
|
+
def config_file
|
71
|
+
File.join(home_directory, "settings.json")
|
72
|
+
end
|
73
|
+
|
74
|
+
def history_file
|
75
|
+
File.join(home_directory, "history.log")
|
76
|
+
end
|
77
|
+
|
78
|
+
def log_file
|
79
|
+
File.join(home_directory, "debug.log")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|