Skip to content

Add history support #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Async, event-driven and UTF-8 aware standard console input & output (STDIN, STDO
* [Echo](#echo)
* [Input buffer](#input-buffer)
* [Cursor](#cursor)
* [History](#history)
* [Advanced](#advanced)
* [Stdout](#stdout)
* [Stdin](#stdin)
Expand Down Expand Up @@ -283,6 +284,99 @@ For example, to move the cursor one character to the left, simply call:
$readline->moveCursorBy(-1);
```

#### History

By default, users can access the history of previous commands by using their
UP and DOWN cursor keys on the keyboard.
The history will start with an empty state, thus this feature is effectively
disabled, as the UP and DOWN cursor keys have no function then.

Note that the history is not maintained automatically.
Any input the user submits by hitting enter will *not* be added to the history
automatically.
This may seem inconvenient at first, but it actually gives you more control over
what (and when) lines should be added to the history.
If you want to automatically add everything from the user input to the history,
you may want to use something like this:

```php
$readline->on('data', function ($line) use ($readline) {
$all = $readline->listHistory();

// skip empty line and duplicate of previous line
if (trim($line) !== '' && $line !== end($all)) {
$readline->addHistory($line);
}
});
```

The `listHistory(): string[]` method can be used to
return an array with all lines in the history.
This will be an empty array until you add new entries via `addHistory()`.

```php
$list = $readline->listHistory();

assert(count($list) === 0);
```

The `addHistory(string $line): Readline` method can be used to
add a new line to the (bottom position of the) history list.
A following `listHistory()` call will return this line as the last element.

```php
$readline->addHistory('a');
$readline->addHistory('b');

$list = $readline->listHistory();
assert($list === array('a', 'b'));
```

The `clearHistory(): Readline` method can be used to
clear the complete history list.
A following `listHistory()` call will return an empty array until you add new
entries via `addHistory()` again.
Note that the history feature will effectively be disabled if the history is
empty, as the UP and DOWN cursor keys have no function then.

```php
$readline->clearHistory();

$list = $readline->listHistory();
assert(count($list) === 0);
```

The `limitHistory(?int $limit): Readline` method can be used to
set a limit of history lines to keep in memory.
By default, only the last 500 lines will be kept in memory and everything else
will be discarded.
You can use an integer value to limit this to the given number of entries or
use `null` for an unlimited number (not recommended, because everything is
kept in RAM).
If you set the limit to `0` (int zero), the history will effectively be
disabled, as no lines can be added to or returned from the history list.
If you're building a CLI application, you may also want to use something like
this to obey the `HISTSIZE` environment variable:

```php
$limit = getenv('HISTSIZE');
if ($limit === '' || $limit < 0) {
// empty string or negative value means unlimited
$readline->limitHistory(null);
} elseif ($limit !== false) {
// apply any other value if given
$readline->limitHistory($limit);
}
```

There is no such thing as a `readHistory()` or `writeHistory()` method
because filesystem operations are inherently blocking and thus beyond the scope
of this library.
Using your favorite filesystem API and an appropriate number of `addHistory()`
or a single `listHistory()` call respectively should be fairly straight
forward and is left up as an exercise for the reader of this documentation
(i.e. *you*).

### Advanced

#### Stdout
Expand Down
25 changes: 23 additions & 2 deletions examples/periodic.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,29 @@
$loop = React\EventLoop\Factory::create();

$stdio = new Stdio($loop);

$stdio->getReadline()->setPrompt('> ');
$readline = $stdio->getReadline();

$readline->setPrompt('> ');

// limit history to HISTSIZE env
$limit = getenv('HISTSIZE');
if ($limit === '' || $limit < 0) {
// empty string or negative value means unlimited
$readline->limitHistory(null);
} elseif ($limit !== false) {
// apply any other value if given
$readline->limitHistory($limit);
}

// add all lines from input to history
$readline->on('data', function ($line) use ($readline) {
$all = $readline->listHistory();

// skip empty line and duplicate of previous line
if (trim($line) !== '' && $line !== end($all)) {
$readline->addHistory($line);
}
});

$stdio->writeln('Will print periodic messages until you type "quit" or "exit"');

Expand Down
23 changes: 0 additions & 23 deletions src/History.php

This file was deleted.

113 changes: 99 additions & 14 deletions src/Readline.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ class Readline extends EventEmitter implements ReadableStreamInterface
private $echo = true;
private $autocomplete = null;
private $move = true;
private $history = null;
private $encoding = 'utf-8';

private $input;
private $output;
private $sequencer;
private $closed = false;

private $historyLines = array();
private $historyPosition = null;
private $historyUnsaved = null;
private $historyLimit = 500;

public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output)
{
$this->input = $input;
Expand Down Expand Up @@ -311,17 +315,73 @@ public function getInput()
}

/**
* set history handler to use (or none)
* Adds a new line to the (bottom position of the) history list
*
* The history handler will be called whenever the user hits the UP or DOWN
* arrow keys.
* @param string $line
* @return self
* @uses self::limitHistory() to make sure list does not exceed limits
*/
public function addHistory($line)
{
$this->historyLines []= $line;

return $this->limitHistory($this->historyLimit);
}

/**
* Clears the complete history list
*
* @param HistoryInterface|null $history
* @return self
*/
public function setHistory(HistoryInterface $history = null)
public function clearHistory()
{
$this->historyLines = array();
$this->historyPosition = null;

if ($this->historyUnsaved !== null) {
$this->setInput($this->historyUnsaved);
$this->historyUnsaved = null;
}

return $this;
}

/**
* Returns an array with all lines in the history
*
* @return string[]
*/
public function listHistory()
{
$this->history = $history;
return $this->historyLines;
}

/**
* Limits the history to a maximum of N entries and truncates the current history list accordingly
*
* @param int|null $limit
* @return self
*/
public function limitHistory($limit)
{
$this->historyLimit = $limit === null ? null : (int)$limit;

// limit send and currently exceeded
if ($this->historyLimit !== null && isset($this->historyLines[$this->historyLimit])) {
// adjust position in history according to new position after applying limit
if ($this->historyPosition !== null) {
$this->historyPosition -= count($this->historyLines) - $this->historyLimit;

// current position will drop off from list => restore original
if ($this->historyPosition < 0) {
$this->setInput($this->historyUnsaved);
$this->historyPosition = null;
$this->historyUnsaved = null;
}
}

$this->historyLines = array_slice($this->historyLines, -$this->historyLimit, $this->historyLimit);
}

return $this;
}
Expand Down Expand Up @@ -468,16 +528,40 @@ public function onKeyRight()
/** @internal */
public function onKeyUp()
{
if ($this->history !== null) {
$this->history->up();
// ignore if already at top or history is empty
if ($this->historyPosition === 0 || !$this->historyLines) {
return;
}

if ($this->historyPosition === null) {
// first time up => move to last entry
$this->historyPosition = count($this->historyLines) - 1;
$this->historyUnsaved = $this->getInput();
} else {
// somewhere in the list => move by one
$this->historyPosition--;
}

$this->setInput($this->historyLines[$this->historyPosition]);
}

/** @internal */
public function onKeyDown()
{
if ($this->history !== null) {
$this->history->down();
// ignore if not currently cycling through history
if ($this->historyPosition === null) {
return;
}

if (isset($this->historyLines[$this->historyPosition + 1])) {
// this is still a valid position => advance by one and apply
$this->historyPosition++;
$this->setInput($this->historyLines[$this->historyPosition]);
} else {
// moved beyond bottom => restore original unsaved input
$this->setInput($this->historyUnsaved);
$this->historyPosition = null;
$this->historyUnsaved = null;
}
}

Expand Down Expand Up @@ -537,6 +621,10 @@ public function deleteChar($n)
*/
protected function processLine()
{
// reset history cycle position
$this->historyPosition = null;
$this->historyUnsaved = null;

// store and reset/clear/redraw current input
$line = $this->linebuffer;
if ($line !== '') {
Expand All @@ -548,9 +636,6 @@ protected function processLine()
}

// process stored input buffer
if ($this->history !== null) {
$this->history->addLine($line);
}
$this->emit('data', array($line));
}

Expand Down
Loading