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