LinuxQuestions.org
Visit Jeremy's Blog.
Go Back   LinuxQuestions.org > Blogs > rainbowsally
User Name
Password

Notices

Rate this Entry

lqMiniTerm (version 2)

Posted 06-15-2013 at 09:07 AM by rainbowsally
Updated 09-04-2013 at 12:13 PM by rainbowsally

Today's Feature
  • A new version of the lqMiniTerm (introduced in the horrible sockets experiments). :-) (If you missed those, count yourself lucky. ;-) )

Unfortunately, for the GUI stuff the file sets are getting rather large. It's only 12K as a gzipped tar but uncompressed it's big enough to make the blog choke, even as an sfxz compressed self extractor.

But it's worth taking a peek at this because there are a few features in it that work (that's important... that it works, because not all of the features do!) so let's talk about this app-main file a little bit.

http://rainbowsally.net/rainbowsally...erm-v02.tar.gz

Unpack it, run 'make' and it should build. If you have trouble run 'make ui' and then make. Don't run 'make update' or 'mc2 -u' unless all the moc_* and ui_* files have been created. (see src/qt_cmds.txt, which is executed by 'make ui'.)

There are several snips in the other files you might find worth grabbing as well. For example, how to move the cursor in a text edit widget several steps in one move. This is not the same as setting the position and qt4 has made moveCursor() so stupid that you can't advance past a "prompt" on the left side easily... but I'm giving away what we're up to already. :-)

It seemed to us here at the Computer Mad Science lab that there should be no good reason why we couldn't subclass a QPlainTextEdit to act like a terminal. We've tried this many times in many different ways and it's almost difficult enough to make you feel ill.

In addition, we wanted a kind of widget that had a windows-like API for reasons that may become clear in time. For now, just consider it making QT a bit more 'generic'. And you can modify this if you'd prefer to use the conventional QT approach.

1. Not in the main file but in the lqMiniTerm code, we center the window on the first showEvent(). That's one snippet you might want to grab if not any of the others.

2. In the main file below, we use ye olde c-style oop to call the lqMiniTerm widget called 'term' (aka hWnd in Windows) to print text to the term from anywhere.

The implementation of term->append() is not in the main file either, but it's noteworthy because if you're an eternal QT4 beginner like me (boy, I hate the way they do things) you might not have realized that QPlainTextEdit::append() always creates an extra newline. insert() doesn't. But it's misnamed "append()" in this code because... that's what it does. As it would "append" a string.

3. There are two parts that run somewhat independently. There's the loop_func and the handler parts, which are both linked when the lqMiniTerm object is created.

a. The loop_func is similar to Windows message dispatcher, but it's simpler. But like Windows it's capable of running at full-bore if you need speed. It is called by the lqMiniTerm::run(timeout = -1) which if set to timeout = 0, will hog as much cpu time as you can stand. :-)

b. The lqMiniTerm::handlerFunc(obj, msg, wparam, lparam) function, on the other hand is only called on signals and events. This is similar to the Windows WNDPROC and in the case of 'events' it works exactly the same, though for signals, the return value is ignored. Generally it should return 'true' if it's been handled (thus you can eat keys on keypress events by returning 'true') and you have some control over the order of execution of things, but this is getting complicated to describe. Just play around with the lqTermEdit event dispatcher and maybe add more events you can mess with.

The lqMiniTerm is the editor widget used in the Designer (*.ui) file. It's promoted from a QPlainTextEdit with only one new feature, and that's the keyPressEvent() dispacther that passes the events to the lqMiniTerm handler function so we can make sure lines don't split if we hit enter when in the middle of a line.

4. We have created a micro-interpreter for the example. When the input string is 'ready', the loop function runs the interpreter to do a command. (Or two commands, if you can find the second one in the code).

When interpreting we do not want to accept input. When inputting we do not want to accept interpreter outputs to the screen.

That's where it gets tricky, and though there are still a few problems to solve, it looks like this implementation might be fairly fast even as the text buffer grows. (That's been a big hangup with our previous attempts to make a read/writable text edit widget.)

file: src/app-main.cpp
purpose: source file
Code:
#include <QtGui/QApplication>
#include "lqminiterm.h"
#include "ui_lqminiterm.h"
#include <ctype.h> // alnum()
#include <stdio.h>

void dbg(){}


static bool input_ready;
static char* input_str;
// static bool text_mode;

static void term_print(lqMiniTerm* term, const char* buf, bool isPrompt = false);

static void term_print(lqMiniTerm* term, const char* buf, bool isPrompt)
{
  term->append(buf, isPrompt);
}

static void prompt(lqMiniTerm* term)
{
  bool isPrompt;  
  term_print(term, "!@#$%%#@>", isPrompt = true);
}

static void test_interp(lqMiniTerm* term, const char* str)
{
  char buf[256];
  
  if(0 == strcmp(str, "ls"))
  {
    FILE* fp = popen("ls", "r");
    if(!fp)
      return;
    while(!feof(fp))
    {
      *buf = 0;
      fgets(buf, 256, fp);
      term->append(buf);
    }
    fclose(fp);
  }
  else if(0 == strcmp(str, "exit"))
  {
    term->append("See yuh later!\n");
    term->update();
    // I/O pause, to allow the OS's other threads to 
    // catch up, including X11.  1 ms should be adequate.
    usleep(1000);
    
    // now we want to keep control of the program flow 
    // here so we process events (so the text can show)
    // and then exit.
    qApp->processEvents();
    sleep(2.5);        
    term->close();
  }
    
  return;
}

// user programmable loop func in app-main()
// this just does shell commands.
extern "C" long loop_func(lqMiniTerm* term)
{
  long handled = 1; // for clarity below
  
  // avoid recursion
  if(term->closing)
    return handled;
  
  // only takes input in edit mode
  if(!term->edit_mode)
    return handled;
  
  // input_ready is set by onNewline in the handler
  if(!input_ready)  
    return handled;
  
  // Ready to go now...
  
  // Typically we'd run an interpreter with the text input
  // so far.
  
  test_interp(term, input_str);
  
  // no feedback to react to so we're done with this loop.
  
  // reset for next and print the prompt
  input_ready = false;  
  prompt(term);  
  
  // get more input from the editor
  term->edit_mode = true;
  
  // and we're done until next loop which is called by
  // lqMiniTerm::run()
  
  return handled;
}


#define UNUSED(x) x=x
extern "C" long handler(lqMiniTerm* term, uint msg, long wparam, void* lparam)
{
  UNUSED(lparam);
  
  int prompt_min = term->prompt_start;
  int prompt_max = term->prompt_end;
  int pos = term->position();

  switch(msg)
  {    
    case msg_init:
      term_print(term,
          "This is a simple interpreter that only does 'ls' in PWD\n"
          "as an example of possible usage.\n\n"
          "Other text will simply be ignored except for one more\n"
          "mystery command that you'll have to find in the sources ;-).\n\n");
      
      term->edit_mode = true; // use as input
      break;
      
    case msg_onKeyPress:
    {
      QTextCursor chk = term->editor()->textCursor();
      if(! term)
      {
        fprintf(stderr, "No term set in EventRelay class\n");
        abort();
      }
      
      QKeyEvent* e = (QKeyEvent*)lparam; // cast generic QEvent to QKeyEvent
                  
      switch(e->key())
      {
        case 0x1000014: // Qt::Key_Right: 
        {
          // skip over prompt area
          if(pos == (prompt_min - 1)) // move to end of prompt and eat key
          {            
            term->setCursorPosition(prompt_max);
            return true; // handled = eat key
          }
          break; // not handled
        }
        case 0x1000003:     // Qt::Key_Backspace:
        {
          // don't stray into prompt area
          if(pos == prompt_max)
            return true;    // handled = eat key
          break;            // not handled
        }
        case 0x1000012:     // Qt::Key_Left:
        {
          if(pos == prompt_max)
          {
            // skip back past prompt start to previous block
            /// might need to be -2 here, not sure how the lines are handled
            /// but we want to go up one block and to the end of the line
            term->setCursorPosition(prompt_min - 1);
            return true;
          }
          break;
        }

        case 0x1000010: // Qt::Key_Home:
        {
          if(pos >= prompt_min)
          {
            // Notes:
            // QPlainTextEdit(1,1,1,1,1);
            // QTextCursor(1,1,1,1,1);
            term->setCursorPosition(prompt_max);
            return true; // handled
          }
          break; // not handled
        }        
        case 0x1000005: // Qt::Key_Enter: (keypad)
        case 0x1000004: // Qt::Key_Return:
        {
          // done always in the lqtermedit KeyPressEvent dispatcher
          // term->editor()->moveCursor(QTextCursor::EndOfLine);
          break;
        }
        // other stuff to handle would be ENTER move if in read only
        // Ctrl-V move before paste if in read only
      }
      
      return false; // not handled
    }
    
    case msg_onClose:
      // printf("Handled: Closing\n");
      term->hide(); // signal to runner we can quit
      term->close();
      break;
    
    case msg_onNewline: // typical newline handler
    {
      if(!term->edit_mode)
        break;
      
      // This is redundant but harmless
      term->moveCursor(QTextCursor::EndOfLine);
      pos = term->position(); // now

      // Now we work with the loop func to get valid input
      const char* s = term->getInput();
      
      int prompt_len = prompt_max - prompt_min;
      int len = strlen(s) - prompt_len;
      
      // A test routine for the commandline to see what we've got
      if((len > 0) && !input_ready)
      {
        input_str = (char*)realloc(input_str, len+1);
        memcpy(input_str, s + prompt_len, len);
        input_str[len] = 0;
        printf("Input: %s\n", input_str);
      }

      // flag LoopFunc that it's ready
      input_ready = true; 
      break;
    }
    case msg_onCursorPositionChanged:
    {
      // if it's on the same line as the prompt limit it to prompt_len offset
      // else make it read-only
      int new_pos = (int)wparam;
      
      if(new_pos < term->prompt_end)
        term->setReadOnly(true);
      else
        term->setReadOnly(false);
    }
  }
  return true; // handled
}



extern void visit_lqTermEdit();  


int main(int argc, char *argv[])
{
  dbg();
  
  visit_lqTermEdit();  
  
  // this sets the global qApp instance
  new QApplication (argc, argv);
  
  lqMiniTerm term(loop_func, handler);
  
  term.show();
  
  // Reposition, resize etc. here after shown()
  
  prompt(&term);
  // instead of "return qApp->exec();" we do...
  return term.run();
}

------------------

I know y'all are probably in love with type safety and all since you appear to be interested in QT programming, but I freaking HATE IT! It makes doing things so hard. No straight shots. And the amount of typing to just do a simple operation is ridiculous. Qt3 was bad but Qt4 is a nighmare.

However, regardless of these problems, and considering that it looks like we CAN take over some of the sluggish functions (such as slot calls, which we can reduce to one slot call for any number of widgets by way of these 'handler' things), the evidence is in. QT can do an excellent job of creating attractive widgets and so it looks like QT will be the GUI toolkit in libLQ for mc2 and other things for the foreseeable future.

------------------

Grab the sources and take a peek at the ui file in Designer. If you have a good debugger, the Makefile is set up with -g3. I like kdbg (version 5.0 or higher). I think you'll find it fun to trace the code and see how it works.

I havent' tried it yet but it should be possible to include it in Designer as a promotion of a QWidget. There's only one slot of interest -- the onExternalInput() slot, which can be wired up to a button or a combo box, to run commands or whatever.

In case you don't get this, from the discussion above or from the example here. The point is to get "strings" from an input and do something with them. What you do is up to you. In this example we just list a directory if the command is spelled right.

And the challenge has been in how to get a text editor to act bidirectionally. Now we just need the cursor to show, even in the 'history' area of the screen so we can copy/paste without needing the mouse in order to get around.

:-)

The Computer Mad Science Team
Posted in Uncategorized
Views 316 Comments 0
« Prev     Main     Next »
Total Comments 0

Comments

 

  



All times are GMT -5. The time now is 08:48 PM.

Main Menu
Advertisement

My LQ
Write for LQ
LinuxQuestions.org is looking for people interested in writing Editorials, Articles, Reviews, and more. If you'd like to contribute content, let us know.
Main Menu
Syndicate
RSS1  Latest Threads
RSS1  LQ News
Twitter: @linuxquestions
identi.ca: @linuxquestions
Facebook: linuxquestions Google+: linuxquestions
Open Source Consulting | Domain Registration