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