/[svn]/linuxsampler/trunk/src/shell/lscp.cpp
ViewVC logotype

Contents of /linuxsampler/trunk/src/shell/lscp.cpp

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2528 - (show annotations) (download)
Mon Mar 3 12:02:40 2014 UTC (10 years, 1 month ago) by schoenebeck
File size: 17857 byte(s)
* LSCP shell: in case of multiple possibilities or non-terminal symbols,
  show them right to the current command line immediately while typing
  (no double tab required for this feature, as it would be the case in
  other shells)
* LSCP shell: fixed sluggish behavior when doing tab auto complete
* LSCP shell: fixed conflicting behavior between keyboard input and
  LSCP server evaluation result, that caused an inconsistent screen
  output (keybord input is now never printed directly on screen, only
  the result returned from LSCP server)

1 /*
2 * LSCP Shell
3 *
4 * Copyright (c) 2014 Christian Schoenebeck
5 *
6 * This program is part of LinuxSampler and released under the same terms.
7 */
8
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <iostream>
12 #include <sstream>
13 #include <string.h>
14
15 #include "LSCPClient.h"
16 #include "KeyboardReader.h"
17 #include "TerminalCtrl.h"
18 #include "CFmt.h"
19 #include "CCursor.h"
20 #include "TerminalPrinter.h"
21
22 #include "../common/global.h"
23 #include "../common/global_private.h"
24 #include "../common/Condition.h"
25
26 #define LSCP_DEFAULT_HOST "localhost"
27 #define LSCP_DEFAULT_PORT 8888
28
29 using namespace std;
30 using namespace LinuxSampler;
31
32 static LSCPClient* g_client = NULL;
33 static KeyboardReader* g_keyboardReader = NULL;
34 static Condition g_todo;
35 static String g_goodPortion;
36 static String g_badPortion;
37 static String g_suggestedPortion;
38 static int g_linesActive = 0;
39 static const String g_prompt = "lscp=# ";
40 static std::vector<String> g_commandHistory;
41 static int g_commandHistoryIndex = -1;
42
43 static void printUsage() {
44 cout << "lscp - The LinuxSampler Control Protocol (LSCP) Shell." << endl;
45 cout << endl;
46 cout << "Usage: lscp [-h HOSTNAME] [-p PORT]" << endl;
47 cout << endl;
48 cout << " -h Host name of LSCP server (default \"" << LSCP_DEFAULT_HOST << "\")." << endl;
49 cout << endl;
50 cout << " -p TCP port number of LSCP server (default " << LSCP_DEFAULT_PORT << ")." << endl;
51 cout << endl;
52 cout << " --no-auto-correct Don't perform auto correction of obvious syntax errors." << endl;
53 cout << endl;
54 }
55
56 static void printWelcome() {
57 cout << "Welcome to lscp " << VERSION << ", the LinuxSampler Control Protocol (LSCP) shell." << endl;
58 cout << endl;
59 }
60
61 static void printPrompt() {
62 cout << g_prompt << flush;
63 }
64
65 static int promptOffset() {
66 return g_prompt.size();
67 }
68
69 // Called by the network reading thread, whenever new data arrived from the
70 // network connection.
71 static void onLSCPClientNewInputAvailable(LSCPClient* client) {
72 g_todo.Set(true);
73 }
74
75 // Called by the keyboard reading thread, whenever a key stroke was received.
76 static void onNewKeyboardInputAvailable(KeyboardReader* reader) {
77 g_todo.Set(true);
78 }
79
80 static void autoComplete() {
81 if (g_suggestedPortion.empty()) return;
82 String s;
83 // let the server delete mistaken characters first
84 for (int i = 0; i < g_badPortion.size(); ++i) s += '\b';
85 // now add the suggested, correct characters
86 s += g_suggestedPortion;
87 g_suggestedPortion.clear();
88 g_client->send(s);
89 }
90
91 static void commandFromHistory(int offset) {
92 if (g_commandHistoryIndex + offset < 0 ||
93 g_commandHistoryIndex + offset >= g_commandHistory.size()) return;
94 g_commandHistoryIndex += offset;
95 int len = g_goodPortion.size() + g_badPortion.size();
96 // erase current active line
97 for (int i = 0; i < len; ++i) g_client->send('\b');
98 // transmit new/old line to LSCP server
99 String command = g_commandHistory[g_commandHistory.size() - g_commandHistoryIndex - 1];
100 g_client->send(command);
101 }
102
103 static void previousCommand() {
104 commandFromHistory(1);
105 }
106
107 static void nextCommand() {
108 commandFromHistory(-1);
109 }
110
111 static void storeCommandInHistory(const String& sCommand) {
112 g_commandHistoryIndex = -1; // reset history index
113 // don't save the command if the previous one was the same
114 if (g_commandHistory.empty() || g_commandHistory.back() != sCommand)
115 g_commandHistory.push_back(sCommand);
116 }
117
118 /**
119 * This LSCP shell application is designed as thin client. That means the heavy
120 * LSCP grammar evaluation tasks are peformed by the LSCP server and the shell
121 * application's task are simply limited to forward individual characters typed
122 * by the user to the LSCP server and showing the result of the LSCP server's
123 * evaluation to the user on the screen. This has the big advantage that the
124 * shell works perfectly with any machine running (some minimum recent version
125 * of) LinuxSampler, no matter which precise LSCP version the server side
126 * is using. Which reduces the maintenance efforts for the shell application
127 * development tremendously.
128 *
129 * As soon as this application established a TCP connection to a LSCP server, it
130 * sends this command to the LSCP server:
131 * @code
132 * SET SHELL INTERACT 1
133 * @endcode
134 * which will inform the LSCP server that this LSCP client is actually a LSCP
135 * shell application. The shell will then simply forward every single character
136 * typed by the user immediately to the LSCP server, which in turn will evaluate
137 * every single character typed by the user and will return immediately a
138 * specially formatted string to the shell application like (assuming the user's
139 * current command line was "CREATE AUasdf"):
140 * @code
141 * SHU:1:CREATE AU{{GF}}asdf{{CU}}{{SB}}DIO_OUTPUT_DEVICE
142 * @endcode
143 * which informs this shell application about the result of the LSCP grammar
144 * evaluation and allows the shell to easily show that result of the evaluation
145 * to the user on the screen. In the example reply above, the prefix "SHU:" just
146 * indicates to the shell application that this response line is the result
147 * of the latest grammar evaluation, the number followed (here 1) indicates the
148 * semantic status of the current command line:
149 *
150 * - 0: Command line is complete, thus ENTER key may be hit by the user now.
151 * - 1: Current command line contains syntax error(s).
152 * - 2: Command line is incomplete, but contains no syntax errors so far.
153 *
154 * Then the actual current command line follows, with special markers:
155 *
156 * - Left of "{{GF}}" the command line is syntactically correct, right of that
157 * marker the command line is syntactically wrong.
158 *
159 * - Marker "{{CU}}" indicates the current cursor position of the command line.
160 *
161 * - Right of "{{SB}}" follows the current auto completion suggestion, so that
162 * string portion was not typed by the user yet, but is expected to be typed
163 * by him next to retain syntax correctness.
164 */
165 int main(int argc, char *argv[]) {
166 String host = LSCP_DEFAULT_HOST;
167 int port = LSCP_DEFAULT_PORT;
168 bool autoCorrect = true;
169
170 // parse command line arguments
171 for (int i = 0; i < argc; ++i) {
172 String s = argv[i];
173 if (s == "-h" || s == "--host") {
174 if (++i >= argc) {
175 printUsage();
176 return -1;
177 }
178 host = argv[i];
179 } else if (s == "-p" || s == "--port") {
180 if (++i >= argc) {
181 printUsage();
182 return -1;
183 }
184 port = atoi(argv[i]);
185 if (port <= 0) {
186 cerr << "Error: invalid port argument \"" << argv[i] << "\"\n";
187 return -1;
188 }
189 } else if (s == "--no-auto-correct") {
190 autoCorrect = false;
191 } else if (s[0] == '-') { // invalid / unknown command line argument ...
192 printUsage();
193 return -1;
194 }
195 }
196
197 // try to connect to the sampler's LSCP server and start a thread for
198 // receiving incoming network data from the sampler's LSCP server
199 g_client = new LSCPClient;
200 g_client->setCallback(onLSCPClientNewInputAvailable);
201 if (!g_client->connect(host, port)) return -1;
202 String sResponse = g_client->sendCommandSync(
203 (autoCorrect) ? "SET SHELL AUTO_CORRECT 1" : "SET SHELL AUTO_CORRECT 0"
204 );
205 sResponse = g_client->sendCommandSync("SET SHELL INTERACT 1");
206 if (sResponse.substr(0, 2) != "OK") {
207 cerr << "Error: sampler too old, it does not support shell instructions\n";
208 return -1;
209 }
210
211 printWelcome();
212 printPrompt();
213
214 // start a thread for reading from the local text input keyboard
215 // (keyboard echo will be disabled as well to have a clean control on what
216 // is appearing on the screen)
217 g_keyboardReader = new KeyboardReader;
218 g_keyboardReader->setCallback(onNewKeyboardInputAvailable);
219 g_keyboardReader->startReading();
220
221 int iKbdEscapeCharsExpected = 0;
222 char kbdPrevEscapeChar;
223
224 // main thread's loop
225 //
226 // This application runs 3 threads:
227 //
228 // - Keyboard thread: reads constantly on stdin for new characters (which
229 // will block this keyboard thread until new character(s) were typed by
230 // the user) and pushes the typed characters into a FIFO buffer.
231 //
232 // - Network thread: reads constantly on the TCP connection for new bytes
233 // being sent by the LSCP server (which will block this network thread
234 // until new bytes were received) and pushes the received bytes into a
235 // FIFO buffer.
236 //
237 // - Main thread: this thread runs in the loop below. The main thread sleeps
238 // (by using the "g_todo" semaphore) until either new keys on the keyboard
239 // were stroke by the user or until new bytes were received from the LSCP
240 // server. The main thread will then accordingly send the typed characters
241 // to the LSCP server and/or show the result of the LSCP server's latest
242 // evaluation to the user on the screen (by pulling those data from the
243 // other two thread's FIFO buffers).
244 while (true) {
245 // sleep until either new data from the network or from keyboard arrived
246 g_todo.WaitIf(false);
247 // immediately unset the condition variable and unlock it
248 g_todo.Set(false);
249 g_todo.Unlock();
250
251 // did network data arrive?
252 while (g_client->messageComplete()) {
253 String line = *g_client->popLine();
254 //printf("line '%s'\n", line.c_str());
255 if (line.substr(0,4) == "SHU:") {
256 int code = 0, n = 0;
257 int res = sscanf(line.c_str(), "SHU:%d:%n", &code, &n);
258 if (res >= 1) {
259 String s = line.substr(n);
260
261 // extract portion that is already syntactically correct
262 size_t iGood = s.find(LSCP_SHK_GOOD_FRONT);
263 String sGood = s.substr(0, iGood);
264 if (sGood.find(LSCP_SHK_CURSOR) != string::npos)
265 sGood.erase(sGood.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
266
267 // extract portion that was written syntactically incorrect
268 String sBad = s.substr(iGood + strlen(LSCP_SHK_GOOD_FRONT));
269 if (sBad.find(LSCP_SHK_CURSOR) != string::npos)
270 sBad.erase(sBad.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
271 if (sBad.find(LSCP_SHK_SUGGEST_BACK) != string::npos)
272 sBad.erase(sBad.find(LSCP_SHK_SUGGEST_BACK)); // erase auto suggestion portion
273 if (sBad.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos)
274 sBad.erase(sBad.find(LSCP_SHK_POSSIBILITIES_BACK)); // erase possibilities portion
275
276 // extract portion that is suggested for auto completion
277 String sSuggest;
278 if (s.find(LSCP_SHK_SUGGEST_BACK) != string::npos) {
279 sSuggest = s.substr(s.find(LSCP_SHK_SUGGEST_BACK) + strlen(LSCP_SHK_SUGGEST_BACK));
280 if (sSuggest.find(LSCP_SHK_CURSOR) != string::npos)
281 sSuggest.erase(sSuggest.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
282 if (sSuggest.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos)
283 sSuggest.erase(sSuggest.find(LSCP_SHK_POSSIBILITIES_BACK)); // erase possibilities portion
284 }
285
286 // extract portion that provides all current possibilities
287 // (that is all branches in the current grammar tree)
288 String sPossibilities;
289 if (s.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos) {
290 sPossibilities = s.substr(s.find(LSCP_SHK_POSSIBILITIES_BACK) + strlen(LSCP_SHK_POSSIBILITIES_BACK));
291 }
292
293 // extract current cursor position
294 int cursorColumn = sGood.size();
295 String sCursor = s;
296 if (sCursor.find(LSCP_SHK_GOOD_FRONT) != string::npos)
297 sCursor.erase(sCursor.find(LSCP_SHK_GOOD_FRONT), strlen(LSCP_SHK_GOOD_FRONT)); // erase good/bad marker
298 if (sCursor.find(LSCP_SHK_SUGGEST_BACK) != string::npos)
299 sCursor.erase(sCursor.find(LSCP_SHK_SUGGEST_BACK), strlen(LSCP_SHK_SUGGEST_BACK)); // erase suggestion marker
300 if (sCursor.find(LSCP_SHK_CURSOR) != string::npos)
301 cursorColumn = sCursor.find(LSCP_SHK_CURSOR);
302
303 // store those informations globally for the auto-completion
304 // feature
305 g_goodPortion = sGood;
306 g_badPortion = sBad;
307 g_suggestedPortion = sSuggest;
308
309 //printf("line '%s' good='%s' bad='%s' suggested='%s' cursor=%d\n", line.c_str(), sGood.c_str(), sBad.c_str(), sSuggest.c_str(), cursorColumn);
310
311 // clear current command line on screen
312 // (which may have been printed over several lines)
313 CCursor cursor = CCursor::now().toColumn(0).clearLine();
314 for (int i = 0; i < g_linesActive; ++i)
315 cursor = cursor.down().clearLine();
316 if (g_linesActive) cursor = cursor.up(g_linesActive).toColumn(0);
317 printPrompt();
318
319 // print out the gathered informations on the screen
320 TerminalPrinter p;
321 CFmt cfmt;
322 if (code == LSCP_SHU_COMPLETE) cfmt.bold().green();
323 else cfmt.bold().white();
324 p << sGood;
325 cfmt.reset().red();
326 p << sBad;
327 cfmt.bold().yellow();
328 p << sSuggest;
329 if (!sPossibilities.empty())
330 p << " <- " << sPossibilities;
331
332 // move cursor back to the appropriate input position in
333 // the command line (which may be several lines above)
334 g_linesActive = p.linesAdvanced();
335 if (p.linesAdvanced()) cursor.up(p.linesAdvanced());
336 cursor.toColumn(cursorColumn + promptOffset());
337 }
338 } else if (line.substr(0,2) == "OK") { // single-line response expected ...
339 cout << endl << flush;
340 CFmt cfmt;
341 cfmt.green();
342 cout << line.substr(0,2) << flush;
343 cfmt.reset();
344 cout << line.substr(2) << endl << flush;
345 printPrompt();
346 } else if (line.substr(0,3) == "WRN") { // single-line response expected ...
347 cout << endl << flush;
348 CFmt cfmt;
349 cfmt.yellow();
350 cout << line.substr(0,3) << flush;
351 cfmt.reset();
352 cout << line.substr(3) << endl << flush;
353 printPrompt();
354 } else if (line.substr(0,3) == "ERR") { // single-line response expected ...
355 cout << endl << flush;
356 CFmt cfmt;
357 cfmt.bold().red();
358 cout << line.substr(0,3) << flush;
359 cfmt.reset();
360 cout << line.substr(3) << endl << flush;
361 printPrompt();
362 } else if (g_client->multiLine()) { // multi-line response expected ...
363 cout << endl << flush;
364 while (true) {
365 cout << line << endl << flush;
366 if (line.substr(0, 1) == ".") break;
367 if (!g_client->lineAvailable()) break;
368 line = *g_client->popLine();
369 }
370 printPrompt();
371 } else {
372 cout << endl << line << endl << flush;
373 printPrompt();
374 }
375 }
376
377 // did keyboard input arrive?
378 while (g_keyboardReader->charAvailable()) {
379 char c = g_keyboardReader->popChar();
380
381 //std::cout << c << "(" << int(c) << ")" << std::endl << std::flush;
382 if (iKbdEscapeCharsExpected) { // escape sequence (still) expected now ...
383 iKbdEscapeCharsExpected--;
384 if (iKbdEscapeCharsExpected) kbdPrevEscapeChar = c;
385 else { // escape sequence is complete ...
386 if (kbdPrevEscapeChar == 91 && c == 65) // up key
387 previousCommand();
388 else if (kbdPrevEscapeChar == 91 && c == 66) // down key
389 nextCommand();
390 else if (kbdPrevEscapeChar == 91 && c == 68) { // left key
391 //TODO: move cursor left
392 } else if (kbdPrevEscapeChar == 91 && c == 67) { // right key
393 //TODO: move cursor right
394 }
395 }
396 continue; // don't send this escape sequence character to LSCP server
397 } else if (c == KBD_ESCAPE) { // escape sequence for special keys expected next ...
398 iKbdEscapeCharsExpected = 2;
399 continue; // don't send ESC character to LSCP server
400 } else if (c == KBD_BACKSPACE) {
401 c = '\b';
402 } else if (c == '\t') { // auto completion ...
403 autoComplete();
404 continue; // don't send tab character to LSCP server
405 } else if (c == '\n') {
406 storeCommandInHistory(g_goodPortion + g_badPortion);
407 }
408
409 g_client->send(c);
410 }
411 }
412
413 return 0;
414 }

  ViewVC Help
Powered by ViewVC