/[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 2534 - (show annotations) (download)
Sun Mar 9 21:34:03 2014 UTC (10 years, 2 months ago) by schoenebeck
File size: 23110 byte(s)
* LSCP shell (WIP): Added initial support for built-in LSCP reference
  documentation, which will automatically show the relevant LSCP reference
  section on screen as soon as one specific LSCP command was detected while
  typing on the command line.
* Bumped version (1.0.0.svn37).

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 static String g_doc;
43
44 static void printUsage() {
45 cout << "lscp - The LinuxSampler Control Protocol (LSCP) Shell." << endl;
46 cout << endl;
47 cout << "Usage: lscp [-h HOSTNAME] [-p PORT]" << endl;
48 cout << endl;
49 cout << " -h Host name of LSCP server (default \"" << LSCP_DEFAULT_HOST << "\")." << endl;
50 cout << endl;
51 cout << " -p TCP port number of LSCP server (default " << LSCP_DEFAULT_PORT << ")." << endl;
52 cout << endl;
53 cout << " --no-auto-correct Don't perform auto correction of obvious syntax errors." << endl;
54 cout << endl;
55 cout << " --no-doc Don't show LSCP reference documentation on screen." << endl;
56 cout << endl;
57 }
58
59 static void printWelcome() {
60 cout << "Welcome to lscp " << VERSION << ", the LinuxSampler Control Protocol (LSCP) shell." << endl;
61 cout << endl;
62 }
63
64 static void printPrompt() {
65 cout << g_prompt << flush;
66 }
67
68 static int promptOffset() {
69 return g_prompt.size();
70 }
71
72 static void quitApp(int code = 0) {
73 std::cout << std::endl << std::flush;
74 if (g_client) delete g_client;
75 if (g_keyboardReader) delete g_keyboardReader;
76 exit(code);
77 }
78
79 // Called by the network reading thread, whenever new data arrived from the
80 // network connection.
81 static void onLSCPClientNewInputAvailable(LSCPClient* client) {
82 g_todo.Set(true);
83 }
84
85 // Called by the keyboard reading thread, whenever a key stroke was received.
86 static void onNewKeyboardInputAvailable(KeyboardReader* reader) {
87 g_todo.Set(true);
88 }
89
90 // Called on network error or when server side closed the TCP connection.
91 static void onLSCPClientErrorOccured(LSCPClient* client) {
92 quitApp();
93 }
94
95 /// Will be called when the user hits tab key to trigger auto completion.
96 static void autoComplete() {
97 if (g_suggestedPortion.empty()) return;
98 String s;
99 // let the server delete mistaken characters first
100 for (int i = 0; i < g_badPortion.size(); ++i) s += '\b';
101 // now add the suggested, correct characters
102 s += g_suggestedPortion;
103 g_suggestedPortion.clear();
104 g_client->send(s);
105 }
106
107 static void commandFromHistory(int offset) {
108 if (g_commandHistoryIndex + offset < 0 ||
109 g_commandHistoryIndex + offset >= g_commandHistory.size()) return;
110 g_commandHistoryIndex += offset;
111 int len = g_goodPortion.size() + g_badPortion.size();
112 // erase current active line
113 for (int i = 0; i < len; ++i) g_client->send('\b');
114 // transmit new/old line to LSCP server
115 String command = g_commandHistory[g_commandHistory.size() - g_commandHistoryIndex - 1];
116 g_client->send(command);
117 }
118
119 /// Will be called when the user hits arrow up key, to iterate to an older command line.
120 static void previousCommand() {
121 commandFromHistory(1);
122 }
123
124 /// Will be called when the user hits arrow down key, to iterate to a more recent command line.
125 static void nextCommand() {
126 commandFromHistory(-1);
127 }
128
129 /// Will be called whenever the user hits ENTER, to store the line in the command history.
130 static void storeCommandInHistory(const String& sCommand) {
131 g_commandHistoryIndex = -1; // reset history index
132 // don't save the command if the previous one was the same
133 if (g_commandHistory.empty() || g_commandHistory.back() != sCommand)
134 g_commandHistory.push_back(sCommand);
135 }
136
137 /// Splits the given string into individual lines for the given screen resolution.
138 static std::vector<String> splitForScreen(const String& s, int cols, int rows) {
139 std::vector<String> lines;
140 if (rows <= 0 || cols <= 0) return lines;
141 String line;
142 for (int i = 0; i < s.size(); ++i) {
143 char c = s[i];
144 if (c == '\r') continue;
145 if (c == '\n') {
146 lines.push_back(line);
147 if (lines.size() >= rows) return lines;
148 line.clear();
149 continue;
150 }
151 line += c;
152 if (line.size() >= cols) {
153 lines.push_back(line);
154 if (lines.size() >= rows) return lines;
155 line.clear();
156 }
157 }
158 return lines;
159 }
160
161 /**
162 * Will be called whenever the LSCP documentation reference to be shown, has
163 * been changed. This call will accordingly update the screen with the new
164 * documentation text received from LSCP server.
165 */
166 static void updateDoc() {
167 const int vOffset = 2;
168
169 CCursor originalCursor = CCursor::now();
170 CCursor cursor = originalCursor;
171 cursor.toColumn(0).down(vOffset);
172
173 // wipe out current documentation off screen
174 cursor.clearVerticalToBottom();
175
176 if (g_doc.empty()) {
177 // restore original cursor position
178 cursor.up(vOffset).toColumn(originalCursor.column());
179 return;
180 }
181
182 // get screen size (in characters)
183 const int cols = TerminalCtrl::columns();
184 const int rows = TerminalCtrl::rows();
185
186 // convert the string block into individual lines according to screen resolution
187 std::vector<String> lines = splitForScreen(g_doc, cols - 1, rows);
188
189 // print lines onto screen
190 for (int row = 0; row < lines.size(); ++row)
191 std::cout << lines[row] << std::endl;
192
193 // restore original cursor position
194 cursor.up(vOffset + lines.size()).toColumn(originalCursor.column());
195 }
196
197 /**
198 * This LSCP shell application is designed as thin client. That means the heavy
199 * LSCP grammar evaluation tasks are peformed by the LSCP server and the shell
200 * application's task are simply limited to forward individual characters typed
201 * by the user to the LSCP server and showing the result of the LSCP server's
202 * evaluation to the user on the screen. This has the big advantage that the
203 * shell works perfectly with any machine running (some minimum recent version
204 * of) LinuxSampler, no matter which precise LSCP version the server side
205 * is using. Which reduces the maintenance efforts for the shell application
206 * development tremendously.
207 *
208 * As soon as this application established a TCP connection to a LSCP server, it
209 * sends this command to the LSCP server:
210 * @code
211 * SET SHELL INTERACT 1
212 * @endcode
213 * which will inform the LSCP server that this LSCP client is actually a LSCP
214 * shell application. The shell will then simply forward every single character
215 * typed by the user immediately to the LSCP server. The LSCP server in turn
216 * will evaluate every single character received and will return immediately a
217 * specially formatted string to the shell application like (assuming the user's
218 * current command line was "CREATE AUasdf"):
219 * @code
220 * SHU:1:CREATE AU{{GF}}asdf{{CU}}{{SB}}DIO_OUTPUT_DEVICE
221 * @endcode
222 * which informs this shell application about the result of the LSCP grammar
223 * evaluation and allows the shell to easily show that result of the evaluation
224 * to the user on the screen. In the example reply above, the prefix "SHU:" just
225 * indicates to the shell application that this response line is the result
226 * of the latest grammar evaluation, the number followed (here 1) indicates the
227 * semantic status of the current command line:
228 *
229 * - 0: Command line is complete, thus ENTER key may be hit by the user now.
230 * - 1: Current command line contains syntax error(s).
231 * - 2: Command line is incomplete, but contains no syntax errors so far.
232 *
233 * Then the actual current command line follows, with special markers:
234 *
235 * - Left of "{{GF}}" the command line is syntactically correct, right of that
236 * marker the command line is syntactically wrong.
237 *
238 * - Marker "{{CU}}" indicates the current cursor position of the command line.
239 *
240 * - Right of "{{SB}}" follows the current auto completion suggestion, so that
241 * string portion was not typed by the user yet, but is expected to be typed
242 * by him next, to retain syntax correctness. The auto completion portion is
243 * added by the LSCP server only if there is one unique way to add characters
244 * to the current command line. If there are multiple possibilities, than this
245 * portion is missing due to ambiguity.
246 *
247 * - Optionally there might also be a "{{PB}" marker on right hand side of the
248 * line. The text right to that marker reflects all possibilities at the
249 * user's current input position (which cannot be auto completed) due to
250 * ambiguity, including abstract (a.k.a. "non-terminal") symbols like:
251 * @code
252 * <digit>, <text>, <number>, etc.
253 * @endcode
254 * This portion is added by the LSCP server only if there is not a unique way
255 * to add characters to the current command line.
256 */
257 int main(int argc, char *argv[]) {
258 String host = LSCP_DEFAULT_HOST;
259 int port = LSCP_DEFAULT_PORT;
260 bool autoCorrect = true;
261 bool showDoc = true;
262
263 // parse command line arguments
264 for (int i = 0; i < argc; ++i) {
265 String s = argv[i];
266 if (s == "-h" || s == "--host") {
267 if (++i >= argc) {
268 printUsage();
269 return -1;
270 }
271 host = argv[i];
272 } else if (s == "-p" || s == "--port") {
273 if (++i >= argc) {
274 printUsage();
275 return -1;
276 }
277 port = atoi(argv[i]);
278 if (port <= 0) {
279 cerr << "Error: invalid port argument \"" << argv[i] << "\"\n";
280 return -1;
281 }
282 } else if (s == "--no-auto-correct") {
283 autoCorrect = false;
284 } else if (s == "--no-doc") {
285 showDoc = false;
286 } else if (s[0] == '-') { // invalid / unknown command line argument ...
287 printUsage();
288 return -1;
289 }
290 }
291
292 // try to connect to the sampler's LSCP server and start a thread for
293 // receiving incoming network data from the sampler's LSCP server
294 g_client = new LSCPClient;
295 g_client->setErrorCallback(onLSCPClientErrorOccured);
296 if (!g_client->connect(host, port)) return -1;
297 g_client->sendCommandSync(
298 (showDoc) ? "SET SHELL DOC 1" : "SET SHELL DOC 0"
299 );
300 String sResponse = g_client->sendCommandSync(
301 (autoCorrect) ? "SET SHELL AUTO_CORRECT 1" : "SET SHELL AUTO_CORRECT 0"
302 );
303 sResponse = g_client->sendCommandSync("SET SHELL INTERACT 1");
304 if (sResponse.substr(0, 2) != "OK") {
305 cerr << "Error: sampler too old, it does not support shell instructions\n";
306 return -1;
307 }
308 g_client->setCallback(onLSCPClientNewInputAvailable);
309
310 printWelcome();
311 printPrompt();
312
313 // start a thread for reading from the local text input keyboard
314 // (keyboard echo will be disabled as well to have a clean control on what
315 // is appearing on the screen)
316 g_keyboardReader = new KeyboardReader;
317 g_keyboardReader->setCallback(onNewKeyboardInputAvailable);
318 g_keyboardReader->startReading();
319
320 int iKbdEscapeCharsExpected = 0;
321 char kbdPrevEscapeChar;
322
323 // main thread's loop
324 //
325 // This application runs 3 threads:
326 //
327 // - Keyboard thread: reads constantly on stdin for new characters (which
328 // will block this keyboard thread until new character(s) were typed by
329 // the user) and pushes the typed characters into a FIFO buffer.
330 //
331 // - Network thread: reads constantly on the TCP connection for new bytes
332 // being sent by the LSCP server (which will block this network thread
333 // until new bytes were received) and pushes the received bytes into a
334 // FIFO buffer.
335 //
336 // - Main thread: this thread runs in the loop below. The main thread sleeps
337 // (by using the "g_todo" condition variable) until either new keys on the
338 // keyboard were stroke by the user or until new bytes were received from
339 // the LSCP server. The main thread will then accordingly send the typed
340 // characters to the LSCP server and/or show the result of the LSCP
341 // server's latest evaluation to the user on the screen (by pulling those
342 // data from the other two thread's FIFO buffers).
343 while (true) {
344 // sleep until either new data from the network or from keyboard arrived
345 g_todo.WaitIf(false);
346 // immediately unset the condition variable and unlock it
347 g_todo.Set(false);
348 g_todo.Unlock();
349
350 // did network data arrive?
351 while (g_client->messageComplete()) {
352 String line = *g_client->popLine();
353 //printf("line '%s'\n", line.c_str());
354 if (line.substr(0,4) == "SHU:") {
355 int code = 0, n = 0;
356 int res = sscanf(line.c_str(), "SHU:%d:%n", &code, &n);
357 if (res >= 1) {
358 String s = line.substr(n);
359
360 // extract portion that is already syntactically correct
361 size_t iGood = s.find(LSCP_SHK_GOOD_FRONT);
362 String sGood = s.substr(0, iGood);
363 if (sGood.find(LSCP_SHK_CURSOR) != string::npos)
364 sGood.erase(sGood.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
365
366 // extract portion that was written syntactically incorrect
367 String sBad = s.substr(iGood + strlen(LSCP_SHK_GOOD_FRONT));
368 if (sBad.find(LSCP_SHK_CURSOR) != string::npos)
369 sBad.erase(sBad.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
370 if (sBad.find(LSCP_SHK_SUGGEST_BACK) != string::npos)
371 sBad.erase(sBad.find(LSCP_SHK_SUGGEST_BACK)); // erase auto suggestion portion
372 if (sBad.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos)
373 sBad.erase(sBad.find(LSCP_SHK_POSSIBILITIES_BACK)); // erase possibilities portion
374
375 // extract portion that is suggested for auto completion
376 String sSuggest;
377 if (s.find(LSCP_SHK_SUGGEST_BACK) != string::npos) {
378 sSuggest = s.substr(s.find(LSCP_SHK_SUGGEST_BACK) + strlen(LSCP_SHK_SUGGEST_BACK));
379 if (sSuggest.find(LSCP_SHK_CURSOR) != string::npos)
380 sSuggest.erase(sSuggest.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
381 if (sSuggest.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos)
382 sSuggest.erase(sSuggest.find(LSCP_SHK_POSSIBILITIES_BACK)); // erase possibilities portion
383 }
384
385 // extract portion that provides all current possibilities
386 // (that is all branches in the current grammar tree)
387 String sPossibilities;
388 if (s.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos) {
389 sPossibilities = s.substr(s.find(LSCP_SHK_POSSIBILITIES_BACK) + strlen(LSCP_SHK_POSSIBILITIES_BACK));
390 }
391
392 // extract current cursor position
393 int cursorColumn = sGood.size();
394 String sCursor = s;
395 if (sCursor.find(LSCP_SHK_GOOD_FRONT) != string::npos)
396 sCursor.erase(sCursor.find(LSCP_SHK_GOOD_FRONT), strlen(LSCP_SHK_GOOD_FRONT)); // erase good/bad marker
397 if (sCursor.find(LSCP_SHK_SUGGEST_BACK) != string::npos)
398 sCursor.erase(sCursor.find(LSCP_SHK_SUGGEST_BACK), strlen(LSCP_SHK_SUGGEST_BACK)); // erase suggestion marker
399 if (sCursor.find(LSCP_SHK_CURSOR) != string::npos)
400 cursorColumn = sCursor.find(LSCP_SHK_CURSOR);
401
402 // store those informations globally for the auto-completion
403 // feature
404 g_goodPortion = sGood;
405 g_badPortion = sBad;
406 g_suggestedPortion = sSuggest;
407
408 //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);
409
410 // clear current command line on screen
411 // (which may have been printed over several lines)
412 CCursor cursor = CCursor::now().toColumn(0).clearLine();
413 for (int i = 0; i < g_linesActive; ++i)
414 cursor = cursor.down().clearLine();
415 if (g_linesActive) cursor = cursor.up(g_linesActive).toColumn(0);
416 printPrompt();
417
418 // print out the gathered informations on the screen
419 TerminalPrinter p;
420 CFmt cfmt;
421 if (code == LSCP_SHU_COMPLETE) cfmt.bold().green();
422 else cfmt.bold().white();
423 p << sGood;
424 cfmt.reset().red();
425 p << sBad;
426 cfmt.bold().yellow();
427 p << sSuggest;
428 if (!sPossibilities.empty())
429 p << " <- " << sPossibilities;
430
431 // move cursor back to the appropriate input position in
432 // the command line (which may be several lines above)
433 g_linesActive = p.linesAdvanced();
434 if (p.linesAdvanced()) cursor.up(p.linesAdvanced());
435 cursor.toColumn(cursorColumn + promptOffset());
436 }
437 } else if (line.substr(0,4) == "SHD:") { // new LSCP doc reference section received ...
438 int code = LSCP_SHD_NO_MATCH;
439 int res = sscanf(line.c_str(), "SHD:%d", &code);
440 g_doc.clear();
441 if (code == LSCP_SHD_MATCH) {
442 while (true) { // multi-line response expected (terminated by dot line) ...
443 if (line.substr(0, 1) == ".") break;
444 if (!g_client->lineAvailable()) break;
445 line = *g_client->popLine();
446 g_doc += line;
447 }
448 }
449 updateDoc();
450 } else if (line.substr(0,2) == "OK") { // single-line response expected ...
451 cout << endl << flush;
452
453 // wipe out potential current documentation off screen
454 CCursor cursor = CCursor::now();
455 cursor.clearVerticalToBottom();
456
457 CFmt cfmt;
458 cfmt.green();
459 cout << line.substr(0,2) << flush;
460 cfmt.reset();
461 cout << line.substr(2) << endl << flush;
462 printPrompt();
463 } else if (line.substr(0,3) == "WRN") { // single-line response expected ...
464 cout << endl << flush;
465
466 // wipe out potential current documentation off screen
467 CCursor cursor = CCursor::now();
468 cursor.clearVerticalToBottom();
469
470 CFmt cfmt;
471 cfmt.yellow();
472 cout << line.substr(0,3) << flush;
473 cfmt.reset();
474 cout << line.substr(3) << endl << flush;
475 printPrompt();
476 } else if (line.substr(0,3) == "ERR") { // single-line response expected ...
477 cout << endl << flush;
478
479 // wipe out potential current documentation off screen
480 CCursor cursor = CCursor::now();
481 cursor.clearVerticalToBottom();
482
483 CFmt cfmt;
484 cfmt.bold().red();
485 cout << line.substr(0,3) << flush;
486 cfmt.reset();
487 cout << line.substr(3) << endl << flush;
488 printPrompt();
489 } else if (g_client->multiLine()) { // multi-line response expected ...
490 cout << endl << flush;
491
492 // wipe out potential current documentation off screen
493 CCursor cursor = CCursor::now();
494 cursor.clearVerticalToBottom();
495
496 while (true) {
497 cout << line << endl << flush;
498 if (line.substr(0, 1) == ".") break;
499 if (!g_client->lineAvailable()) break;
500 line = *g_client->popLine();
501 }
502 printPrompt();
503 } else {
504 cout << endl << flush;
505
506 // wipe out potential current documentation off screen
507 CCursor cursor = CCursor::now();
508 cursor.clearVerticalToBottom();
509
510 cout << line << endl << flush;
511 printPrompt();
512 }
513 }
514
515 // did keyboard input arrive?
516 while (g_keyboardReader->charAvailable()) {
517 char c = g_keyboardReader->popChar();
518
519 //std::cout << c << "(" << int(c) << ")" << std::endl << std::flush;
520 if (iKbdEscapeCharsExpected) { // escape sequence (still) expected now ...
521 iKbdEscapeCharsExpected--;
522 if (iKbdEscapeCharsExpected) kbdPrevEscapeChar = c;
523 else { // escape sequence is complete ...
524 if (kbdPrevEscapeChar == 91 && c == 65) // up key
525 previousCommand();
526 else if (kbdPrevEscapeChar == 91 && c == 66) // down key
527 nextCommand();
528 else if (kbdPrevEscapeChar == 91 && c == 68) // left key
529 g_client->send(2); // custom usage of this ASCII code
530 else if (kbdPrevEscapeChar == 91 && c == 67) // right key
531 g_client->send(3); // custom usage of this ASCII code
532 }
533 continue; // don't send this escape sequence character to LSCP server
534 } else if (c == KBD_ESCAPE) { // escape sequence for special keys expected next ...
535 iKbdEscapeCharsExpected = 2;
536 continue; // don't send ESC character to LSCP server
537 } else if (c == KBD_BACKSPACE) {
538 c = '\b';
539 } else if (c == '\t') { // auto completion ...
540 autoComplete();
541 continue; // don't send tab character to LSCP server
542 } else if (c == '\n') {
543 storeCommandInHistory(g_goodPortion + g_badPortion);
544 }
545
546 g_client->send(c);
547 }
548 }
549
550 return 0;
551 }

  ViewVC Help
Powered by ViewVC