/* * LSCP Shell * * Copyright (c) 2014 Christian Schoenebeck * * This program is part of LinuxSampler and released under the same terms. */ #include #include #include #include #include #include "LSCPClient.h" #include "KeyboardReader.h" #include "TerminalCtrl.h" #include "CFmt.h" #include "CCursor.h" #include "../common/global.h" #include "../common/global_private.h" #include "../common/Condition.h" #define LSCP_DEFAULT_HOST "localhost" #define LSCP_DEFAULT_PORT 8888 using namespace std; using namespace LinuxSampler; static LSCPClient* g_client = NULL; static KeyboardReader* g_keyboardReader = NULL; static Condition g_todo; static String g_goodPortion; static String g_badPortion; static String g_suggestedPortion; static const String g_prompt = "lscp=# "; static void printUsage() { cout << "lscp - The LinuxSampler Control Protocol (LSCP) Shell." << endl; cout << endl; cout << "Usage: lscp [-h HOSTNAME] [-p PORT]" << endl; cout << endl; cout << " -h Host name of LSCP server (default \"" << LSCP_DEFAULT_HOST << "\")." << endl; cout << endl; cout << " -p TCP port number of LSCP server (default " << LSCP_DEFAULT_PORT << ")." << endl; cout << endl; cout << " --no-auto-correct Don't perform auto correction of obvious syntax errors." << endl; cout << endl; } static void printWelcome() { cout << "Welcome to lscp " << VERSION << ", the LinuxSampler Control Protocol (LSCP) shell." << endl; cout << endl; } static void printPrompt() { cout << g_prompt << flush; } static int promptOffset() { return g_prompt.size(); } // Called by the network reading thread, whenever new data arrived from the // network connection. static void onLSCPClientNewInputAvailable(LSCPClient* client) { g_todo.Set(true); } // Called by the keyboard reading thread, whenever a key stroke was received. static void onNewKeyboardInputAvailable(KeyboardReader* reader) { g_todo.Set(true); } static void autoComplete() { if (g_suggestedPortion.empty()) return; String s; // let the server delete mistaken characters first for (int i = 0; i < g_badPortion.size(); ++i) s += '\b'; // now add the suggested, correct characters s += g_suggestedPortion; g_suggestedPortion.clear(); g_client->send(s); } int main(int argc, char *argv[]) { String host = LSCP_DEFAULT_HOST; int port = LSCP_DEFAULT_PORT; bool autoCorrect = true; // parse command line arguments for (int i = 0; i < argc; ++i) { String s = argv[i]; if (s == "-h" || s == "--host") { if (++i >= argc) { printUsage(); return -1; } host = argv[i]; } else if (s == "-p" || s == "--port") { if (++i >= argc) { printUsage(); return -1; } port = atoi(argv[i]); if (port <= 0) { cerr << "Error: invalid port argument \"" << argv[i] << "\"\n"; return -1; } } else if (s == "--no-auto-correct") { autoCorrect = false; } else if (s[0] == '-') { // invalid / unknown command line argument ... printUsage(); return -1; } } // try to connect to the sampler's LSCP server and start a thread for // receiving incoming network data from the sampler's LSCP server g_client = new LSCPClient; g_client->setCallback(onLSCPClientNewInputAvailable); if (!g_client->connect(host, port)) return -1; String sResponse = g_client->sendCommandSync( (autoCorrect) ? "SET SHELL AUTO_CORRECT 1" : "SET SHELL AUTO_CORRECT 0" ); sResponse = g_client->sendCommandSync("SET SHELL INTERACT 1"); if (sResponse.substr(0, 2) != "OK") { cerr << "Error: sampler too old, it does not support shell instructions\n"; return -1; } printWelcome(); printPrompt(); // start a thread for reading from the local text input keyboard // (keyboard echo will be disabled as well to have a clean control on what // is appearing on the screen) g_keyboardReader = new KeyboardReader; g_keyboardReader->setCallback(onNewKeyboardInputAvailable); g_keyboardReader->startReading(); // main thread's loop while (true) { // sleep until either new data from the network or from keyboard arrived g_todo.WaitIf(false); // immediately unset the condition variable and unlock it g_todo.Set(false); g_todo.Unlock(); // did network data arrive? while (g_client->messageComplete()) { String line = *g_client->popLine(); //printf("line '%s'\n", line.c_str()); if (line.substr(0,4) == "SHU:") { int code = 0, n = 0; int res = sscanf(line.c_str(), "SHU:%d:%n", &code, &n); if (res >= 1) { String s = line.substr(n); // extract portion that is already syntactically correct size_t iGood = s.find(LSCP_SHK_GOOD_FRONT); String sGood = s.substr(0, iGood); if (sGood.find(LSCP_SHK_CURSOR) != string::npos) sGood.erase(sGood.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker // extract portion that was written syntactically incorrect String sBad = s.substr(iGood + strlen(LSCP_SHK_GOOD_FRONT)); if (sBad.find(LSCP_SHK_CURSOR) != string::npos) sBad.erase(sBad.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker if (sBad.find(LSCP_SHK_SUGGEST_BACK) != string::npos) sBad.erase(sBad.find(LSCP_SHK_SUGGEST_BACK)); // erase auto suggestion portion // extract portion that is suggested for auto completion String sSuggest; if (s.find(LSCP_SHK_SUGGEST_BACK) != string::npos) { sSuggest = s.substr(s.find(LSCP_SHK_SUGGEST_BACK) + strlen(LSCP_SHK_SUGGEST_BACK)); if (sSuggest.find(LSCP_SHK_CURSOR) != string::npos) sSuggest.erase(sSuggest.find(LSCP_SHK_CURSOR), strlen(LSCP_SHK_CURSOR)); // erase cursor marker } // extract current cursor position int cursorColumn = sGood.size(); String sCursor = s; if (sCursor.find(LSCP_SHK_GOOD_FRONT) != string::npos) sCursor.erase(sCursor.find(LSCP_SHK_GOOD_FRONT), strlen(LSCP_SHK_GOOD_FRONT)); // erase good/bad marker if (sCursor.find(LSCP_SHK_SUGGEST_BACK) != string::npos) sCursor.erase(sCursor.find(LSCP_SHK_SUGGEST_BACK), strlen(LSCP_SHK_SUGGEST_BACK)); // erase suggestion marker if (sCursor.find(LSCP_SHK_CURSOR) != string::npos) cursorColumn = sCursor.find(LSCP_SHK_CURSOR); // store those informations globally for the auto-completion // feature g_goodPortion = sGood; g_badPortion = sBad; g_suggestedPortion = sSuggest; //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); CCursor cursor = CCursor::now().toColumn(0).clearLine(); printPrompt(); CFmt cfmt; if (code == LSCP_SHU_COMPLETE) cfmt.bold().green(); else cfmt.bold().white(); cout << sGood << flush; cfmt.reset().red(); cout << sBad << flush; cfmt.bold().yellow(); cout << sSuggest << flush; cursor.toColumn(cursorColumn + promptOffset()); } } else if (line.substr(0,2) == "OK") { // single-line response expected ... cout << endl << flush; CFmt cfmt; cfmt.green(); cout << line.substr(0,2) << flush; cfmt.reset(); cout << line.substr(2) << endl << flush; printPrompt(); } else if (line.substr(0,3) == "WRN") { // single-line response expected ... cout << endl << flush; CFmt cfmt; cfmt.yellow(); cout << line.substr(0,3) << flush; cfmt.reset(); cout << line.substr(3) << endl << flush; printPrompt(); } else if (line.substr(0,3) == "ERR") { // single-line response expected ... cout << endl << flush; CFmt cfmt; cfmt.bold().red(); cout << line.substr(0,3) << flush; cfmt.reset(); cout << line.substr(3) << endl << flush; printPrompt(); } else if (g_client->multiLine()) { // multi-line response expected ... cout << endl << flush; while (true) { cout << line << endl << flush; if (line.substr(0, 1) == ".") break; if (!g_client->lineAvailable()) break; line = *g_client->popLine(); } printPrompt(); } else { cout << endl << line << endl << flush; printPrompt(); } } // did keyboard input arrive? while (g_keyboardReader->charAvailable()) { char c = g_keyboardReader->popChar(); CFmt cfmt; cfmt.white(); //std::cout << c << "(" << int(c) << ")" << std::endl << std::flush; if (c == KBD_BACKSPACE) { if (promptOffset() < CCursor::now().column()) cout << "\b \b" << flush; c = '\b'; } else if (c == '\t') { // auto completion ... autoComplete(); continue; // don't send tab character to LSCP server } else if (c != '\n') { // don't apply RETURN stroke yet, since the typed command might still be corrected by the sampler cout << c << flush; } g_client->send(c); } } return 0; }