/[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 2518 - (hide annotations) (download)
Sat Feb 8 00:49:30 2014 UTC (10 years, 2 months ago) by schoenebeck
File size: 12924 byte(s)
* LSCP shell (WIP): added support for browsing the command
  history with up / down keys.
* LSCP server: fixed compilation error with Bison 3.x.
* Bumped version (1.0.0.svn31).

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    
21     #include "../common/global.h"
22 schoenebeck 2517 #include "../common/global_private.h"
23 schoenebeck 2515 #include "../common/Condition.h"
24    
25     #define LSCP_DEFAULT_HOST "localhost"
26     #define LSCP_DEFAULT_PORT 8888
27    
28     using namespace std;
29     using namespace LinuxSampler;
30    
31 schoenebeck 2517 static LSCPClient* g_client = NULL;
32     static KeyboardReader* g_keyboardReader = NULL;
33 schoenebeck 2515 static Condition g_todo;
34 schoenebeck 2516 static String g_goodPortion;
35     static String g_badPortion;
36     static String g_suggestedPortion;
37 schoenebeck 2517 static const String g_prompt = "lscp=# ";
38 schoenebeck 2518 static std::vector<String> g_commandHistory;
39     static int g_commandHistoryIndex = -1;
40 schoenebeck 2515
41     static void printUsage() {
42     cout << "lscp - The LinuxSampler Control Protocol (LSCP) Shell." << endl;
43     cout << endl;
44     cout << "Usage: lscp [-h HOSTNAME] [-p PORT]" << endl;
45     cout << endl;
46     cout << " -h Host name of LSCP server (default \"" << LSCP_DEFAULT_HOST << "\")." << endl;
47     cout << endl;
48     cout << " -p TCP port number of LSCP server (default " << LSCP_DEFAULT_PORT << ")." << endl;
49     cout << endl;
50 schoenebeck 2516 cout << " --no-auto-correct Don't perform auto correction of obvious syntax errors." << endl;
51     cout << endl;
52 schoenebeck 2515 }
53    
54 schoenebeck 2517 static void printWelcome() {
55     cout << "Welcome to lscp " << VERSION << ", the LinuxSampler Control Protocol (LSCP) shell." << endl;
56     cout << endl;
57     }
58    
59     static void printPrompt() {
60     cout << g_prompt << flush;
61     }
62    
63     static int promptOffset() {
64     return g_prompt.size();
65     }
66    
67 schoenebeck 2515 // Called by the network reading thread, whenever new data arrived from the
68     // network connection.
69     static void onLSCPClientNewInputAvailable(LSCPClient* client) {
70     g_todo.Set(true);
71     }
72    
73     // Called by the keyboard reading thread, whenever a key stroke was received.
74     static void onNewKeyboardInputAvailable(KeyboardReader* reader) {
75     g_todo.Set(true);
76     }
77    
78 schoenebeck 2516 static void autoComplete() {
79     if (g_suggestedPortion.empty()) return;
80     String s;
81     // let the server delete mistaken characters first
82     for (int i = 0; i < g_badPortion.size(); ++i) s += '\b';
83     // now add the suggested, correct characters
84     s += g_suggestedPortion;
85     g_suggestedPortion.clear();
86 schoenebeck 2517 g_client->send(s);
87 schoenebeck 2516 }
88    
89 schoenebeck 2518 static void commandFromHistory(int offset) {
90     if (g_commandHistoryIndex + offset < 0 ||
91     g_commandHistoryIndex + offset >= g_commandHistory.size()) return;
92     g_commandHistoryIndex += offset;
93     int len = g_goodPortion.size() + g_badPortion.size();
94     // erase current active line
95     for (int i = 0; i < len; ++i) g_client->send('\b');
96     // transmit new/old line to LSCP server
97     String command = g_commandHistory[g_commandHistory.size() - g_commandHistoryIndex - 1];
98     g_client->send(command);
99     }
100    
101     static void previousCommand() {
102     commandFromHistory(1);
103     }
104    
105     static void nextCommand() {
106     commandFromHistory(-1);
107     }
108    
109     static void storeCommandInHistory(const String& sCommand) {
110     g_commandHistoryIndex = -1; // reset history index
111     // don't save the command if the previous one was the same
112     if (g_commandHistory.empty() || g_commandHistory.back() != sCommand)
113     g_commandHistory.push_back(sCommand);
114     }
115    
116 schoenebeck 2515 int main(int argc, char *argv[]) {
117     String host = LSCP_DEFAULT_HOST;
118     int port = LSCP_DEFAULT_PORT;
119 schoenebeck 2516 bool autoCorrect = true;
120 schoenebeck 2515
121     // parse command line arguments
122     for (int i = 0; i < argc; ++i) {
123     String s = argv[i];
124     if (s == "-h" || s == "--host") {
125     if (++i >= argc) {
126     printUsage();
127     return -1;
128     }
129     host = argv[i];
130     } else if (s == "-p" || s == "--port") {
131     if (++i >= argc) {
132     printUsage();
133     return -1;
134     }
135     port = atoi(argv[i]);
136     if (port <= 0) {
137     cerr << "Error: invalid port argument \"" << argv[i] << "\"\n";
138     return -1;
139     }
140 schoenebeck 2516 } else if (s == "--no-auto-correct") {
141     autoCorrect = false;
142 schoenebeck 2515 } else if (s[0] == '-') { // invalid / unknown command line argument ...
143     printUsage();
144     return -1;
145     }
146     }
147    
148     // try to connect to the sampler's LSCP server and start a thread for
149     // receiving incoming network data from the sampler's LSCP server
150 schoenebeck 2517 g_client = new LSCPClient;
151     g_client->setCallback(onLSCPClientNewInputAvailable);
152     if (!g_client->connect(host, port)) return -1;
153     String sResponse = g_client->sendCommandSync(
154 schoenebeck 2516 (autoCorrect) ? "SET SHELL AUTO_CORRECT 1" : "SET SHELL AUTO_CORRECT 0"
155     );
156 schoenebeck 2517 sResponse = g_client->sendCommandSync("SET SHELL INTERACT 1");
157 schoenebeck 2515 if (sResponse.substr(0, 2) != "OK") {
158     cerr << "Error: sampler too old, it does not support shell instructions\n";
159     return -1;
160     }
161 schoenebeck 2517
162     printWelcome();
163     printPrompt();
164 schoenebeck 2515
165     // start a thread for reading from the local text input keyboard
166     // (keyboard echo will be disabled as well to have a clean control on what
167     // is appearing on the screen)
168 schoenebeck 2517 g_keyboardReader = new KeyboardReader;
169     g_keyboardReader->setCallback(onNewKeyboardInputAvailable);
170     g_keyboardReader->startReading();
171 schoenebeck 2518
172     int iKbdEscapeCharsExpected = 0;
173     char kbdPrevEscapeChar;
174    
175 schoenebeck 2515 // main thread's loop
176     while (true) {
177     // sleep until either new data from the network or from keyboard arrived
178     g_todo.WaitIf(false);
179     // immediately unset the condition variable and unlock it
180     g_todo.Set(false);
181     g_todo.Unlock();
182    
183     // did network data arrive?
184 schoenebeck 2517 while (g_client->messageComplete()) {
185     String line = *g_client->popLine();
186 schoenebeck 2515 //printf("line '%s'\n", line.c_str());
187     if (line.substr(0,4) == "SHU:") {
188     int code = 0, n = 0;
189     int res = sscanf(line.c_str(), "SHU:%d:%n", &code, &n);
190     if (res >= 1) {
191     String s = line.substr(n);
192    
193 schoenebeck 2516 // extract portion that is already syntactically correct
194     size_t iGood = s.find(LSCP_SHK_GOOD_FRONT);
195     String sGood = s.substr(0, iGood);
196     if (sGood.find(LSCP_SHK_CURSOR) != string::npos)
197     sGood.erase(sGood.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
198 schoenebeck 2515
199 schoenebeck 2516 // extract portion that was written syntactically incorrect
200     String sBad = s.substr(iGood + strlen(LSCP_SHK_GOOD_FRONT));
201     if (sBad.find(LSCP_SHK_CURSOR) != string::npos)
202     sBad.erase(sBad.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
203     if (sBad.find(LSCP_SHK_SUGGEST_BACK) != string::npos)
204     sBad.erase(sBad.find(LSCP_SHK_SUGGEST_BACK)); // erase auto suggestion portion
205 schoenebeck 2515
206 schoenebeck 2516 // extract portion that is suggested for auto completion
207     String sSuggest;
208     if (s.find(LSCP_SHK_SUGGEST_BACK) != string::npos) {
209     sSuggest = s.substr(s.find(LSCP_SHK_SUGGEST_BACK) + strlen(LSCP_SHK_SUGGEST_BACK));
210     if (sSuggest.find(LSCP_SHK_CURSOR) != string::npos)
211     sSuggest.erase(sSuggest.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker
212     }
213    
214     // extract current cursor position
215     int cursorColumn = sGood.size();
216     String sCursor = s;
217     if (sCursor.find(LSCP_SHK_GOOD_FRONT) != string::npos)
218     sCursor.erase(sCursor.find(LSCP_SHK_GOOD_FRONT), strlen(LSCP_SHK_GOOD_FRONT)); // erase good/bad marker
219     if (sCursor.find(LSCP_SHK_SUGGEST_BACK) != string::npos)
220     sCursor.erase(sCursor.find(LSCP_SHK_SUGGEST_BACK), strlen(LSCP_SHK_SUGGEST_BACK)); // erase suggestion marker
221     if (sCursor.find(LSCP_SHK_CURSOR) != string::npos)
222     cursorColumn = sCursor.find(LSCP_SHK_CURSOR);
223    
224     // store those informations globally for the auto-completion
225     // feature
226     g_goodPortion = sGood;
227     g_badPortion = sBad;
228     g_suggestedPortion = sSuggest;
229    
230     //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);
231    
232     CCursor cursor = CCursor::now().toColumn(0).clearLine();
233 schoenebeck 2517 printPrompt();
234 schoenebeck 2516
235 schoenebeck 2515 CFmt cfmt;
236     if (code == LSCP_SHU_COMPLETE) cfmt.bold().green();
237     else cfmt.bold().white();
238     cout << sGood << flush;
239     cfmt.reset().red();
240     cout << sBad << flush;
241 schoenebeck 2516 cfmt.bold().yellow();
242     cout << sSuggest << flush;
243    
244 schoenebeck 2517 cursor.toColumn(cursorColumn + promptOffset());
245 schoenebeck 2515 }
246     } else if (line.substr(0,2) == "OK") { // single-line response expected ...
247     cout << endl << flush;
248     CFmt cfmt;
249     cfmt.green();
250     cout << line.substr(0,2) << flush;
251     cfmt.reset();
252     cout << line.substr(2) << endl << flush;
253 schoenebeck 2517 printPrompt();
254 schoenebeck 2515 } else if (line.substr(0,3) == "WRN") { // single-line response expected ...
255     cout << endl << flush;
256     CFmt cfmt;
257     cfmt.yellow();
258     cout << line.substr(0,3) << flush;
259     cfmt.reset();
260     cout << line.substr(3) << endl << flush;
261 schoenebeck 2517 printPrompt();
262 schoenebeck 2515 } else if (line.substr(0,3) == "ERR") { // single-line response expected ...
263     cout << endl << flush;
264     CFmt cfmt;
265     cfmt.bold().red();
266     cout << line.substr(0,3) << flush;
267     cfmt.reset();
268     cout << line.substr(3) << endl << flush;
269 schoenebeck 2517 printPrompt();
270     } else if (g_client->multiLine()) { // multi-line response expected ...
271 schoenebeck 2515 cout << endl << flush;
272     while (true) {
273     cout << line << endl << flush;
274     if (line.substr(0, 1) == ".") break;
275 schoenebeck 2517 if (!g_client->lineAvailable()) break;
276     line = *g_client->popLine();
277 schoenebeck 2515 }
278 schoenebeck 2517 printPrompt();
279 schoenebeck 2515 } else {
280     cout << endl << line << endl << flush;
281 schoenebeck 2517 printPrompt();
282 schoenebeck 2515 }
283     }
284    
285     // did keyboard input arrive?
286 schoenebeck 2517 while (g_keyboardReader->charAvailable()) {
287     char c = g_keyboardReader->popChar();
288 schoenebeck 2515
289     CFmt cfmt;
290     cfmt.white();
291     //std::cout << c << "(" << int(c) << ")" << std::endl << std::flush;
292 schoenebeck 2518 if (iKbdEscapeCharsExpected) { // escape sequence (still) expected now ...
293     iKbdEscapeCharsExpected--;
294     if (iKbdEscapeCharsExpected) kbdPrevEscapeChar = c;
295     else { // escape sequence is complete ...
296     if (kbdPrevEscapeChar == 91 && c == 65) // up key
297     previousCommand();
298     else if (kbdPrevEscapeChar == 91 && c == 66) // down key
299     nextCommand();
300     else if (kbdPrevEscapeChar == 91 && c == 68) { // left key
301     //TODO: move cursor left
302     } else if (kbdPrevEscapeChar == 91 && c == 67) { // right key
303     //TODO: move cursor right
304     }
305     }
306     continue; // don't send this escape sequence character to LSCP server
307     } else if (c == KBD_ESCAPE) { // escape sequence for special keys expected next ...
308     iKbdEscapeCharsExpected = 2;
309     continue; // don't send ESC character to LSCP server
310     } else if (c == KBD_BACKSPACE) {
311 schoenebeck 2517 if (promptOffset() < CCursor::now().column())
312     cout << "\b \b" << flush;
313 schoenebeck 2518 c = '\b';
314 schoenebeck 2516 } else if (c == '\t') { // auto completion ...
315     autoComplete();
316     continue; // don't send tab character to LSCP server
317 schoenebeck 2518 } else if (c == '\n') {
318     storeCommandInHistory(g_goodPortion + g_badPortion);
319     } else { // don't apply RETURN stroke yet, since the typed command might still be corrected by the sampler
320 schoenebeck 2515 cout << c << flush;
321     }
322    
323 schoenebeck 2517 g_client->send(c);
324 schoenebeck 2515 }
325     }
326    
327     return 0;
328     }

  ViewVC Help
Powered by ViewVC