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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2531 - (hide annotations) (download)
Wed Mar 5 00:02:21 2014 UTC (10 years, 1 month ago) by schoenebeck
File size: 17886 byte(s)
* LSCP shell: Added support for moving cursor left/right with arrow keys.
* Bumped version (1.0.0.svn35).

1 schoenebeck 2515 /*
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 schoenebeck 2528 #include "TerminalPrinter.h"
21 schoenebeck 2515
22     #include "../common/global.h"
23 schoenebeck 2517 #include "../common/global_private.h"
24 schoenebeck 2515 #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 schoenebeck 2517 static LSCPClient* g_client = NULL;
33     static KeyboardReader* g_keyboardReader = NULL;
34 schoenebeck 2515 static Condition g_todo;
35 schoenebeck 2516 static String g_goodPortion;
36     static String g_badPortion;
37     static String g_suggestedPortion;
38 schoenebeck 2528 static int g_linesActive = 0;
39 schoenebeck 2517 static const String g_prompt = "lscp=# ";
40 schoenebeck 2518 static std::vector<String> g_commandHistory;
41     static int g_commandHistoryIndex = -1;
42 schoenebeck 2515
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 schoenebeck 2516 cout << " --no-auto-correct Don't perform auto correction of obvious syntax errors." << endl;
53     cout << endl;
54 schoenebeck 2515 }
55    
56 schoenebeck 2517 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 schoenebeck 2515 // 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 schoenebeck 2516 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 schoenebeck 2517 g_client->send(s);
89 schoenebeck 2516 }
90    
91 schoenebeck 2518 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 schoenebeck 2525 /**
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 schoenebeck 2515 int main(int argc, char *argv[]) {
166     String host = LSCP_DEFAULT_HOST;
167     int port = LSCP_DEFAULT_PORT;
168 schoenebeck 2516 bool autoCorrect = true;
169 schoenebeck 2515
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 schoenebeck 2516 } else if (s == "--no-auto-correct") {
190     autoCorrect = false;
191 schoenebeck 2515 } 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 schoenebeck 2517 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 schoenebeck 2516 (autoCorrect) ? "SET SHELL AUTO_CORRECT 1" : "SET SHELL AUTO_CORRECT 0"
204     );
205 schoenebeck 2517 sResponse = g_client->sendCommandSync("SET SHELL INTERACT 1");
206 schoenebeck 2515 if (sResponse.substr(0, 2) != "OK") {
207     cerr << "Error: sampler too old, it does not support shell instructions\n";
208     return -1;
209     }
210 schoenebeck 2517
211     printWelcome();
212     printPrompt();
213 schoenebeck 2515
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 schoenebeck 2517 g_keyboardReader = new KeyboardReader;
218     g_keyboardReader->setCallback(onNewKeyboardInputAvailable);
219     g_keyboardReader->startReading();
220 schoenebeck 2518
221     int iKbdEscapeCharsExpected = 0;
222     char kbdPrevEscapeChar;
223    
224 schoenebeck 2515 // main thread's loop
225 schoenebeck 2525 //
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 schoenebeck 2515 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 schoenebeck 2517 while (g_client->messageComplete()) {
253     String line = *g_client->popLine();
254 schoenebeck 2515 //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 schoenebeck 2516 // 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 schoenebeck 2515
267 schoenebeck 2516 // 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 schoenebeck 2528 if (sBad.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos)
274     sBad.erase(sBad.find(LSCP_SHK_POSSIBILITIES_BACK)); // erase possibilities portion
275 schoenebeck 2515
276 schoenebeck 2516 // 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 schoenebeck 2528 if (sSuggest.find(LSCP_SHK_POSSIBILITIES_BACK) != string::npos)
283     sSuggest.erase(sSuggest.find(LSCP_SHK_POSSIBILITIES_BACK)); // erase possibilities portion
284 schoenebeck 2516 }
285    
286 schoenebeck 2528 // 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 schoenebeck 2516 // 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 schoenebeck 2528 // clear current command line on screen
312     // (which may have been printed over several lines)
313 schoenebeck 2516 CCursor cursor = CCursor::now().toColumn(0).clearLine();
314 schoenebeck 2528 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 schoenebeck 2517 printPrompt();
318 schoenebeck 2516
319 schoenebeck 2528 // print out the gathered informations on the screen
320     TerminalPrinter p;
321 schoenebeck 2515 CFmt cfmt;
322     if (code == LSCP_SHU_COMPLETE) cfmt.bold().green();
323     else cfmt.bold().white();
324 schoenebeck 2528 p << sGood;
325 schoenebeck 2515 cfmt.reset().red();
326 schoenebeck 2528 p << sBad;
327 schoenebeck 2516 cfmt.bold().yellow();
328 schoenebeck 2528 p << sSuggest;
329     if (!sPossibilities.empty())
330     p << " <- " << sPossibilities;
331 schoenebeck 2516
332 schoenebeck 2528 // 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 schoenebeck 2517 cursor.toColumn(cursorColumn + promptOffset());
337 schoenebeck 2515 }
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 schoenebeck 2517 printPrompt();
346 schoenebeck 2515 } 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 schoenebeck 2517 printPrompt();
354 schoenebeck 2515 } 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 schoenebeck 2517 printPrompt();
362     } else if (g_client->multiLine()) { // multi-line response expected ...
363 schoenebeck 2515 cout << endl << flush;
364     while (true) {
365     cout << line << endl << flush;
366     if (line.substr(0, 1) == ".") break;
367 schoenebeck 2517 if (!g_client->lineAvailable()) break;
368     line = *g_client->popLine();
369 schoenebeck 2515 }
370 schoenebeck 2517 printPrompt();
371 schoenebeck 2515 } else {
372     cout << endl << line << endl << flush;
373 schoenebeck 2517 printPrompt();
374 schoenebeck 2515 }
375     }
376    
377     // did keyboard input arrive?
378 schoenebeck 2517 while (g_keyboardReader->charAvailable()) {
379     char c = g_keyboardReader->popChar();
380 schoenebeck 2515
381     //std::cout << c << "(" << int(c) << ")" << std::endl << std::flush;
382 schoenebeck 2518 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 schoenebeck 2531 else if (kbdPrevEscapeChar == 91 && c == 68) // left key
391     g_client->send(2); // custom usage of this ASCII code
392     else if (kbdPrevEscapeChar == 91 && c == 67) // right key
393     g_client->send(3); // custom usage of this ASCII code
394 schoenebeck 2518 }
395     continue; // don't send this escape sequence character to LSCP server
396     } else if (c == KBD_ESCAPE) { // escape sequence for special keys expected next ...
397     iKbdEscapeCharsExpected = 2;
398     continue; // don't send ESC character to LSCP server
399     } else if (c == KBD_BACKSPACE) {
400 schoenebeck 2528 c = '\b';
401 schoenebeck 2516 } else if (c == '\t') { // auto completion ...
402     autoComplete();
403     continue; // don't send tab character to LSCP server
404 schoenebeck 2518 } else if (c == '\n') {
405     storeCommandInHistory(g_goodPortion + g_badPortion);
406 schoenebeck 2515 }
407    
408 schoenebeck 2517 g_client->send(c);
409 schoenebeck 2515 }
410     }
411    
412     return 0;
413     }

  ViewVC Help
Powered by ViewVC