From 5c63de281b82bced6ec36b853196641e6efc8cba Mon Sep 17 00:00:00 2001 From: paulc Date: Fri, 24 Feb 2012 18:06:10 +0000 Subject: [PATCH] Implemented line editing and command history in rmanager. git-svn-id: http://voip.null.ro/svn/yate@4945 acf43c95-373e-0410-b603-e72c3f656dc1 --- conf.d/rmanager.conf.sample | 4 + modules/rmanager.cpp | 208 ++++++++++++++++++++++++++++++------ 2 files changed, 182 insertions(+), 30 deletions(-) diff --git a/conf.d/rmanager.conf.sample b/conf.d/rmanager.conf.sample index 862edaa0..ed227c05 100644 --- a/conf.d/rmanager.conf.sample +++ b/conf.d/rmanager.conf.sample @@ -42,6 +42,10 @@ ; This is almost never required and needs Yate to run as superuser ;interactive=no +; maxhistory: int: Number of distinct lines to preserve in the session history +; The Up / Down arrow keys allow cycling through the history +;maxhistory=10 + ; context: string: SSL context to use to secure the connection ; Setting a context enables SSL on the listener and overrides any domain ;context= diff --git a/modules/rmanager.cpp b/modules/rmanager.cpp index b8c6b0f0..80447e59 100644 --- a/modules/rmanager.cpp +++ b/modules/rmanager.cpp @@ -39,6 +39,9 @@ using namespace TelEngine; namespace { // anonymous +#define DEF_HISTORY 10 +#define MAX_HISTORY 50 + typedef struct { const char* name; const char* args; @@ -211,6 +214,8 @@ public: bool autoComplete(); void errorBeep(); void clearLine(); + void writeBuffer(); + void writeBufferTail(bool eraseOne = false); void writeStr(const char *str, int len = -1); void writeDebug(const char *str, int level); void writeEvent(const char *str, int level); @@ -238,8 +243,10 @@ private: u_int64_t m_timeout; String m_buffer; String m_address; - String m_lastcmd; + ObjList m_history; RefPointer m_listener; + unsigned int m_cursorPos; + unsigned int m_histLen; }; class RManager : public Plugin @@ -408,7 +415,7 @@ Connection::Connection(Socket* sock, const char* addr, RManagerListener* listene m_auth(None), m_debug(false), m_output(false), m_colorize(false), m_machine(false), m_threshold(DebugAll), m_socket(sock), m_lastch(0), m_escmode(0), m_echoing(false), m_beeping(false), - m_timeout(0), m_address(addr), m_listener(listener) + m_timeout(0), m_address(addr), m_listener(listener), m_cursorPos(0), m_histLen(DEF_HISTORY) { s_mutex.lock(); s_connList.append(this); @@ -452,6 +459,9 @@ void Connection::run() if (Admin == m_auth) m_debug = cfg().getBoolValue("debug",false); } + m_histLen = cfg().getIntValue("maxhistory",DEF_HISTORY); + if (m_histLen > MAX_HISTORY) + m_histLen = MAX_HISTORY; String hdr = cfg().getValue("header","YATE ${version}-${release} (http://YATE.null.ro) ready on ${nodename}."); Engine::runParams().replaceParams(hdr); if (cfg().getBoolValue("telnet",true)) { @@ -522,6 +532,30 @@ void Connection::clearLine() writeStr("\r\033[K\r"); } +// write just the tail of the current buffer, get back the cursor +void Connection::writeBufferTail(bool eraseOne) +{ + String tail = m_buffer.substr(m_cursorPos); + if (eraseOne) + tail += " "; + writeStr(tail); + // now write enough backspaces to get back + tail.assign('\b',tail.length()); + writeStr(tail); +} + +// write the current buffer, leave the cursor in the right place +void Connection::writeBuffer() +{ + if (m_cursorPos == m_buffer.length()) { + writeStr(m_buffer); + return; + } + if (m_cursorPos) + writeStr(m_buffer,m_cursorPos); + writeBufferTail(); +} + // process incoming telnet characters bool Connection::processTelnetChar(unsigned char c) { @@ -609,6 +643,8 @@ bool Connection::processTelnetChar(unsigned char c) // process incoming terminal characters bool Connection::processChar(unsigned char c) { + bool atEol = (m_buffer.length() == m_cursorPos); + XDebug(DebugAll,"cur=%u len=%u '%s'",m_cursorPos,m_buffer.length(),m_buffer.safe()); switch (c) { case '\0': m_escmode = 0; @@ -620,6 +656,7 @@ bool Connection::processChar(unsigned char c) m_escmode = 0; if (m_buffer.null()) return false; + // fall through case '\r': m_escmode = 0; if (m_echoing) @@ -627,6 +664,7 @@ bool Connection::processChar(unsigned char c) if (processLine(m_buffer)) return true; m_buffer.clear(); + m_cursorPos = 0; return false; case 0x03: // ^C, BREAK m_escmode = 0; @@ -674,21 +712,22 @@ bool Connection::processChar(unsigned char c) if (!m_echoing) break; clearLine(); - writeStr(m_buffer); + writeBuffer(); return false; case 0x15: // ^U if (m_buffer.null()) break; m_escmode = 0; m_buffer.clear(); + m_cursorPos = 0; if (m_echoing) clearLine(); return false; case 0x17: // ^W - if (m_buffer.null()) + if (m_cursorPos <= 0) errorBeep(); else { - int i = m_buffer.length()-1; + int i = m_cursorPos-1; for (; i > 0; i--) if (m_buffer[i] != ' ') break; @@ -698,28 +737,45 @@ bool Connection::processChar(unsigned char c) break; } m_escmode = 0; - m_buffer.assign(m_buffer,i); + m_buffer = m_buffer.substr(0,i) + m_buffer.substr(m_cursorPos); + m_cursorPos = i; if (m_echoing) { clearLine(); - writeStr(m_buffer); + writeBuffer(); } } return false; case 0x7F: // DEL case 0x08: // ^H, BACKSPACE - if (m_buffer.null()) { + if (!m_cursorPos) { errorBeep(); return false; } m_escmode = 0; - m_buffer.assign(m_buffer,m_buffer.length()-1); - if (m_echoing) - writeStr("\b \b"); + if (atEol) { + m_buffer.assign(m_buffer,--m_cursorPos); + if (m_echoing) + writeStr("\b \b"); + } + else { + m_buffer = m_buffer.substr(0,m_cursorPos-1) + m_buffer.substr(m_cursorPos); + m_cursorPos--; + if (m_echoing) { + writeStr("\b"); + writeBufferTail(true); + } + } return false; case 0x09: // ^I, TAB m_escmode = 0; if (m_buffer.null()) return processLine("help",false); + if (!atEol) { + if (m_echoing) + writeStr(m_buffer.c_str()+m_cursorPos,m_buffer.length()-m_cursorPos); + m_cursorPos = m_buffer.length(); + return false; + } if (!autoComplete()) errorBeep(); return false; @@ -742,27 +798,98 @@ bool Connection::processChar(unsigned char c) m_escmode = c; return false; } + char escMode = m_escmode; + m_escmode = 0; DDebug("RManager",DebugInfo,"ANSI '%s%c' last '%s%c'", (c >= ' ') ? "" : "^", (c >= ' ') ? c : c+0x40, - (m_escmode >= ' ') ? "" : "^", (m_escmode >= ' ') ? m_escmode : m_escmode+0x40 + (escMode >= ' ') ? "" : "^", (escMode >= ' ') ? escMode : escMode+0x40 ); - m_escmode = 0; switch (c) { case 'A': // Up arrow + { + String* s = static_cast(m_history.remove(false)); + if (m_buffer) + m_history.append(new String(m_buffer)); + m_buffer = s; + TelEngine::destruct(s); + } + clearLine(); + m_cursorPos = m_buffer.length(); + writeBuffer(); + return false; case 'B': // Down arrow - if (m_lastcmd.null()) { + { + ObjList* l = &m_history; + while (l->skipNext()) + l = l->skipNext(); + String* s = static_cast(l->get()); + m_history.remove(s,false); + if (m_buffer) + m_history.insert(new String(m_buffer)); + m_buffer = s; + TelEngine::destruct(s); + } + clearLine(); + m_cursorPos = m_buffer.length(); + writeBuffer(); + return false; + case 'C': // Right arrow + if (atEol || m_buffer.null()) { errorBeep(); return false; } - else { - String str = m_lastcmd; - if (m_buffer) - m_lastcmd = m_buffer; - m_buffer = str; - } - clearLine(); - writeStr(m_buffer); + if (m_echoing) + writeStr(m_buffer.c_str()+m_cursorPos,1); + m_cursorPos++; return false; + case 'D': // Left arrow + if (!m_cursorPos || m_buffer.null()) { + errorBeep(); + return false; + } + if (m_echoing) + writeStr("\b"); + m_cursorPos--; + return false; + case 'H': // Home + if (m_echoing) + writeStr("\r"); + m_cursorPos = 0; + return false; + case 'F': // End + if (atEol) + return false; + if (m_echoing && m_buffer) + writeStr(m_buffer.c_str()+m_cursorPos); + m_cursorPos = m_buffer.length(); + return false; + case '~': + switch (escMode) { + case '1': // Home + if (m_echoing) + writeStr("\r"); + m_cursorPos = 0; + return false; + case '4': // End + if (atEol) + return false; + if (m_echoing && m_buffer) + writeStr(m_buffer.c_str()+m_cursorPos); + m_cursorPos = m_buffer.length(); + return false; + case '3': // Delete + if (atEol || m_buffer.null()) { + errorBeep(); + return false; + } + m_buffer = m_buffer.substr(0,m_cursorPos) + m_buffer.substr(m_cursorPos+1); + writeBufferTail(true); + return false; + //case '2': // Insert + //case '5': // Page up + //case '6': // Page down + } + break; } c = 0; } @@ -771,14 +898,28 @@ bool Connection::processChar(unsigned char c) return false; } if (m_echoing && (c == ' ')) { - if (m_buffer.null() || m_buffer.endsWith(" ")) { + if (m_buffer.null() || (atEol && m_buffer.endsWith(" "))) { errorBeep(); return false; } } - m_buffer += (char)c; - if (m_echoing) - writeStr((char*)&c,1); + if (atEol) { + // append char + m_buffer += (char)c; + m_cursorPos = m_buffer.length(); + if (m_echoing) + writeStr((char*)&c,1); + } + else { + // insert char + String tmp((char)c); + m_buffer = m_buffer.substr(0,m_cursorPos) + tmp + m_buffer.substr(m_cursorPos); + m_cursorPos++; + if (m_echoing) { + writeStr(tmp); + writeBufferTail(m_cursorPos); + } + } return false; } @@ -893,8 +1034,9 @@ bool Connection::autoComplete() return false; if (m.retValue().find('\t') < 0) { m_buffer = m_buffer.substr(0,keepLen)+m.retValue()+" "; + m_cursorPos = m_buffer.length(); clearLine(); - writeStr(m_buffer); + writeBuffer(); return true; } // more options returned - list them and display the prompt again @@ -919,8 +1061,9 @@ bool Connection::autoComplete() } TelEngine::destruct(l); m_buffer += maxMatch.substr(partWord.length()); + m_cursorPos = m_buffer.length(); writeStr("\r\n"); - writeStr(m_buffer); + writeBuffer(); return true; } @@ -933,8 +1076,13 @@ bool Connection::processLine(const char *line, bool saveLine) if (str.null()) return false; - if (saveLine) - m_lastcmd = str; + if (saveLine) { + m_history.remove(str); + while (GenObject* obj = m_history[m_histLen]) + m_history.remove(obj); + m_history.insert(new String(str)); + } + line = 0; m_buffer.clear();