Skip to content

Commit

Permalink
Shell: Command history in the command line widget
Browse files Browse the repository at this point in the history
Bash-like, editable command history.
  • Loading branch information
skyjake committed Jan 25, 2013
1 parent 27a4d8a commit 73f97c9
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 47 deletions.
173 changes: 129 additions & 44 deletions doomsday/tools/shell/shell-text/src/commandlinewidget.cpp
Expand Up @@ -28,30 +28,86 @@ struct CommandLineWidget::Instance
{
CommandLineWidget &self;
ConstantRule *height;
String command;
int cursorPos;
QList<int> wraps;

/**
* Line of text with a cursor.
*/
struct Command
{
String text;
int cursor; ///< Index in range [0...text.size()]

Command() : cursor(0) {}

void doBackspace()
{
if(!text.isEmpty() && cursor > 0)
{
text.remove(--cursor, 1);
}
}

void doDelete()
{
if(text.size() > cursor)
{
text.remove(cursor, 1);
}
}

void doLeft()
{
if(cursor > 0) --cursor;
}

void doRight()
{
if(cursor < text.size()) ++cursor;
}

void insert(String const &str)
{
text.insert(cursor++, str);
}
};

// Command history.
QList<Command> history;
int historyPos;

// Word wrapping.
struct Span
{
int start;
int end;
bool isFinal;
};
QList<int> wraps;

Instance(CommandLineWidget &cli) : self(cli), cursorPos(0)
Instance(CommandLineWidget &cli) : self(cli), historyPos(0)
{
// Initial height of the command line (1 row).
height = new ConstantRule(1);

wraps.append(0);
history.append(Command());
}

~Instance()
{
releaseRef(height);
}

Command &command()
{
return history[historyPos];
}

Command const &command() const
{
return history[historyPos];
}

/**
* Determines where word wrapping needs to occur and updates the height of
* the widget to accommodate all the needed lines.
Expand All @@ -60,20 +116,22 @@ struct CommandLineWidget::Instance
{
wraps.clear();

int const lineWidth = self.rule().recti().width() - 3;
int const lineWidth = de::max(1, self.rule().recti().width() - 3);

String const &cmd = command().text;

int begin = 0;
forever
{
int end = begin + lineWidth;
if(end >= command.size())
if(end >= cmd.size())
{
// Time to stop.
wraps.append(command.size());
wraps.append(cmd.size());
break;
}
// Find a good break point.
while(!command.at(end).isSpace())
while(!cmd.at(end).isSpace())
{
--end;
if(end == begin)
Expand All @@ -83,7 +141,7 @@ struct CommandLineWidget::Instance
break;
}
}
if(command.at(end).isSpace()) ++end;
if(cmd.at(end).isSpace()) ++end;
wraps.append(end);
begin = end;
}
Expand All @@ -110,11 +168,12 @@ struct CommandLineWidget::Instance
}

/**
* Calculates the visual position of the cursor, including the line that it
* is on.
* Calculates the visual position of the cursor (of the current command),
* including the line that it is on.
*/
de::Vector2i lineCursorPos() const
{
int const cursorPos = command().cursor;
de::Vector2i pos(cursorPos);
for(pos.y = 0; pos.y < wraps.size(); ++pos.y)
{
Expand Down Expand Up @@ -148,9 +207,9 @@ struct CommandLineWidget::Instance

// Move cursor onto the adjacent line.
Span span = lineSpan(linePos.y + lineOff);
cursorPos = span.start + linePos.x;
command().cursor = span.start + linePos.x;
if(!span.isFinal) span.end--;
if(cursorPos > span.end) cursorPos = span.end;
if(command().cursor > span.end) command().cursor = span.end;
return true;
}
};
Expand Down Expand Up @@ -182,21 +241,25 @@ void CommandLineWidget::draw()
TextCanvas *cv = targetCanvas();
if(!cv) return;

de::Rectanglei pos = rule().recti();
Rectanglei pos = rule().recti();

// Temporary buffer for drawing.
TextCanvas buf(pos.size());

TextCanvas::Char::Attribs attr = TextCanvas::Char::Reverse;
TextCanvas::Char bg(' ', attr);
buf.clear(TextCanvas::Char(' ', attr));

cv->fill(pos, bg);
cv->put(pos.topLeft, TextCanvas::Char('>', attr | TextCanvas::Char::Bold));
buf.put(Vector2i(0, 0), TextCanvas::Char('>', attr | TextCanvas::Char::Bold));

// Draw all the lines, wrapped as previously determined.
for(int y = 0; y < d->wraps.size(); ++y)
{
Instance::Span span = d->lineSpan(y);
String part = d->command.substr(span.start, span.end - span.start);
cv->drawText(pos.topLeft + Vector2i(2, y), part, attr);
String part = d->command().text.substr(span.start, span.end - span.start);
buf.drawText(Vector2i(2, y), part, attr);
}

buf.blit(*cv, pos.topLeft);
}

bool CommandLineWidget::handleEvent(Event const *event)
Expand All @@ -210,72 +273,94 @@ bool CommandLineWidget::handleEvent(Event const *event)
// Insert text?
if(!ev->text().isEmpty())
{
d->command.insert(d->cursorPos++, ev->text());
d->command().insert(ev->text());
}
else
{
// Control character.
eaten = handleControlKey(ev->key());
}

d->updateWrapsAndHeight();

root().requestDraw();
if(eaten)
{
d->updateWrapsAndHeight();
root().requestDraw();
}
return eaten;
}

bool CommandLineWidget::handleControlKey(int key)
{
Instance::Command &cmd = d->command();

switch(key)
{
case Qt::Key_Backspace:
if(d->command.size() > 0 && d->cursorPos > 0)
{
d->command.remove(--d->cursorPos, 1);
}
cmd.doBackspace();
return true;

case Qt::Key_Delete:
if(d->command.size() > d->cursorPos)
{
d->command.remove(d->cursorPos, 1);
}
cmd.doDelete();
return true;

case Qt::Key_Left:
if(d->cursorPos > 0) --d->cursorPos;
cmd.doLeft();
return true;

case Qt::Key_Right:
if(d->cursorPos < d->command.size()) ++d->cursorPos;
cmd.doRight();
return true;

case Qt::Key_Home:
d->cursorPos = d->lineSpan(d->lineCursorPos().y).start;
cmd.cursor = d->lineSpan(d->lineCursorPos().y).start;
return true;

case Qt::Key_End:
{
Instance::Span span = d->lineSpan(d->lineCursorPos().y);
d->cursorPos = span.end - (span.isFinal? 0 : 1);
return true;
}
case Qt::Key_End: {
Instance::Span const span = d->lineSpan(d->lineCursorPos().y);
cmd.cursor = span.end - (span.isFinal? 0 : 1);
return true; }

case Qt::Key_K: // assuming Control mod
d->command = d->command.remove(d->cursorPos,
d->lineSpan(d->lineCursorPos().y).end - d->cursorPos);
cmd.text.remove(cmd.cursor, d->lineSpan(d->lineCursorPos().y).end - cmd.cursor);
return true;

case Qt::Key_Up:
// First try moving within the current command.
d->moveCursorByLine(-1);
if(!d->moveCursorByLine(-1))
{
if(d->historyPos > 0) d->historyPos--;
}
return true;

case Qt::Key_Down:
// First try moving within the current command.
d->moveCursorByLine(+1);
if(!d->moveCursorByLine(+1))
{
if(d->historyPos < d->history.size() - 1) d->historyPos++;
}
return true;

case Qt::Key_Enter: {
String entered = cmd.text;

// Update the history.
if(d->historyPos < d->history.size() - 1)
{
if(d->history.last().text.isEmpty())
{
// Prune an empty entry in the end of history.
d->history.removeLast();
}
// Currently back in the history; duplicate the edited entry.
d->history.append(cmd);
}
// Move on.
d->history.append(Instance::Command());
d->historyPos = d->history.size() - 1;

emit commandEntered(entered);
return true; }

default:
break;
}
Expand Down
7 changes: 6 additions & 1 deletion doomsday/tools/shell/shell-text/src/commandlinewidget.h
Expand Up @@ -23,6 +23,8 @@

class CommandLineWidget : public TextWidget
{
Q_OBJECT

public:
/**
* The height rule of the widget is set up during construction.
Expand All @@ -34,12 +36,15 @@ class CommandLineWidget : public TextWidget
virtual ~CommandLineWidget();

de::Vector2i cursorPosition();
bool handleControlKey(int key);

// Events.
void viewResized();
void draw();
bool handleEvent(de::Event const *event);

bool handleControlKey(int key);
signals:
void commandEntered(de::String command);

private:
struct Instance;
Expand Down
5 changes: 5 additions & 0 deletions doomsday/tools/shell/shell-text/src/textcanvas.cpp
Expand Up @@ -142,6 +142,11 @@ void TextCanvas::markDirty()
d->markAllAsDirty(true);
}

void TextCanvas::clear(Char const &ch)
{
fill(de::Rectanglei(de::Vector2i(0, 0), d->size), ch);
}

void TextCanvas::fill(de::Rectanglei const &rect, Char const &ch)
{
for(int y = rect.top(); y < rect.bottom(); ++y)
Expand Down
2 changes: 2 additions & 0 deletions doomsday/tools/shell/shell-text/src/textcanvas.h
Expand Up @@ -120,6 +120,8 @@ class TextCanvas
*/
void markDirty();

void clear(Char const &ch = Char());

void fill(de::Rectanglei const &rect, Char const &ch);

void put(de::Vector2i const &pos, Char const &ch);
Expand Down
8 changes: 6 additions & 2 deletions doomsday/tools/shell/shell-text/src/textwidget.h
Expand Up @@ -21,6 +21,7 @@

#include <de/Widget>
#include <de/RectangleRule>
#include <QObject>
#include "textcanvas.h"

class TextRootWidget;
Expand All @@ -30,12 +31,15 @@ class TextRootWidget;
*
* It is assumed that the root widget under which text widgets are used is
* derived from TextRootWidget.
*
* QObject is a base class for signals and slots.
*/
class TextWidget : public de::Widget
class TextWidget : public QObject, public de::Widget
{
Q_OBJECT

public:
TextWidget(de::String const &name = "");

virtual ~TextWidget();

TextRootWidget &root() const;
Expand Down

0 comments on commit 73f97c9

Please sign in to comment.