Skip to content

Commit 7a0fa02

Browse files
authored
Merge pull request #40 from clue-labs/history
Add history support
2 parents 57e2758 + a40a30e commit 7a0fa02

File tree

5 files changed

+489
-39
lines changed

5 files changed

+489
-39
lines changed

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Async, event-driven and UTF-8 aware standard console input & output (STDIN, STDO
1414
* [Echo](#echo)
1515
* [Input buffer](#input-buffer)
1616
* [Cursor](#cursor)
17+
* [History](#history)
1718
* [Advanced](#advanced)
1819
* [Stdout](#stdout)
1920
* [Stdin](#stdin)
@@ -283,6 +284,99 @@ For example, to move the cursor one character to the left, simply call:
283284
$readline->moveCursorBy(-1);
284285
```
285286

287+
#### History
288+
289+
By default, users can access the history of previous commands by using their
290+
UP and DOWN cursor keys on the keyboard.
291+
The history will start with an empty state, thus this feature is effectively
292+
disabled, as the UP and DOWN cursor keys have no function then.
293+
294+
Note that the history is not maintained automatically.
295+
Any input the user submits by hitting enter will *not* be added to the history
296+
automatically.
297+
This may seem inconvenient at first, but it actually gives you more control over
298+
what (and when) lines should be added to the history.
299+
If you want to automatically add everything from the user input to the history,
300+
you may want to use something like this:
301+
302+
```php
303+
$readline->on('data', function ($line) use ($readline) {
304+
$all = $readline->listHistory();
305+
306+
// skip empty line and duplicate of previous line
307+
if (trim($line) !== '' && $line !== end($all)) {
308+
$readline->addHistory($line);
309+
}
310+
});
311+
```
312+
313+
The `listHistory(): string[]` method can be used to
314+
return an array with all lines in the history.
315+
This will be an empty array until you add new entries via `addHistory()`.
316+
317+
```php
318+
$list = $readline->listHistory();
319+
320+
assert(count($list) === 0);
321+
```
322+
323+
The `addHistory(string $line): Readline` method can be used to
324+
add a new line to the (bottom position of the) history list.
325+
A following `listHistory()` call will return this line as the last element.
326+
327+
```php
328+
$readline->addHistory('a');
329+
$readline->addHistory('b');
330+
331+
$list = $readline->listHistory();
332+
assert($list === array('a', 'b'));
333+
```
334+
335+
The `clearHistory(): Readline` method can be used to
336+
clear the complete history list.
337+
A following `listHistory()` call will return an empty array until you add new
338+
entries via `addHistory()` again.
339+
Note that the history feature will effectively be disabled if the history is
340+
empty, as the UP and DOWN cursor keys have no function then.
341+
342+
```php
343+
$readline->clearHistory();
344+
345+
$list = $readline->listHistory();
346+
assert(count($list) === 0);
347+
```
348+
349+
The `limitHistory(?int $limit): Readline` method can be used to
350+
set a limit of history lines to keep in memory.
351+
By default, only the last 500 lines will be kept in memory and everything else
352+
will be discarded.
353+
You can use an integer value to limit this to the given number of entries or
354+
use `null` for an unlimited number (not recommended, because everything is
355+
kept in RAM).
356+
If you set the limit to `0` (int zero), the history will effectively be
357+
disabled, as no lines can be added to or returned from the history list.
358+
If you're building a CLI application, you may also want to use something like
359+
this to obey the `HISTSIZE` environment variable:
360+
361+
```php
362+
$limit = getenv('HISTSIZE');
363+
if ($limit === '' || $limit < 0) {
364+
// empty string or negative value means unlimited
365+
$readline->limitHistory(null);
366+
} elseif ($limit !== false) {
367+
// apply any other value if given
368+
$readline->limitHistory($limit);
369+
}
370+
```
371+
372+
There is no such thing as a `readHistory()` or `writeHistory()` method
373+
because filesystem operations are inherently blocking and thus beyond the scope
374+
of this library.
375+
Using your favorite filesystem API and an appropriate number of `addHistory()`
376+
or a single `listHistory()` call respectively should be fairly straight
377+
forward and is left up as an exercise for the reader of this documentation
378+
(i.e. *you*).
379+
286380
### Advanced
287381

288382
#### Stdout

examples/periodic.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,29 @@
77
$loop = React\EventLoop\Factory::create();
88

99
$stdio = new Stdio($loop);
10-
11-
$stdio->getReadline()->setPrompt('> ');
10+
$readline = $stdio->getReadline();
11+
12+
$readline->setPrompt('> ');
13+
14+
// limit history to HISTSIZE env
15+
$limit = getenv('HISTSIZE');
16+
if ($limit === '' || $limit < 0) {
17+
// empty string or negative value means unlimited
18+
$readline->limitHistory(null);
19+
} elseif ($limit !== false) {
20+
// apply any other value if given
21+
$readline->limitHistory($limit);
22+
}
23+
24+
// add all lines from input to history
25+
$readline->on('data', function ($line) use ($readline) {
26+
$all = $readline->listHistory();
27+
28+
// skip empty line and duplicate of previous line
29+
if (trim($line) !== '' && $line !== end($all)) {
30+
$readline->addHistory($line);
31+
}
32+
});
1233

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

src/History.php

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/Readline.php

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ class Readline extends EventEmitter implements ReadableStreamInterface
1717
private $echo = true;
1818
private $autocomplete = null;
1919
private $move = true;
20-
private $history = null;
2120
private $encoding = 'utf-8';
2221

2322
private $input;
2423
private $output;
2524
private $sequencer;
2625
private $closed = false;
2726

27+
private $historyLines = array();
28+
private $historyPosition = null;
29+
private $historyUnsaved = null;
30+
private $historyLimit = 500;
31+
2832
public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output)
2933
{
3034
$this->input = $input;
@@ -311,17 +315,73 @@ public function getInput()
311315
}
312316

313317
/**
314-
* set history handler to use (or none)
318+
* Adds a new line to the (bottom position of the) history list
315319
*
316-
* The history handler will be called whenever the user hits the UP or DOWN
317-
* arrow keys.
320+
* @param string $line
321+
* @return self
322+
* @uses self::limitHistory() to make sure list does not exceed limits
323+
*/
324+
public function addHistory($line)
325+
{
326+
$this->historyLines []= $line;
327+
328+
return $this->limitHistory($this->historyLimit);
329+
}
330+
331+
/**
332+
* Clears the complete history list
318333
*
319-
* @param HistoryInterface|null $history
320334
* @return self
321335
*/
322-
public function setHistory(HistoryInterface $history = null)
336+
public function clearHistory()
337+
{
338+
$this->historyLines = array();
339+
$this->historyPosition = null;
340+
341+
if ($this->historyUnsaved !== null) {
342+
$this->setInput($this->historyUnsaved);
343+
$this->historyUnsaved = null;
344+
}
345+
346+
return $this;
347+
}
348+
349+
/**
350+
* Returns an array with all lines in the history
351+
*
352+
* @return string[]
353+
*/
354+
public function listHistory()
323355
{
324-
$this->history = $history;
356+
return $this->historyLines;
357+
}
358+
359+
/**
360+
* Limits the history to a maximum of N entries and truncates the current history list accordingly
361+
*
362+
* @param int|null $limit
363+
* @return self
364+
*/
365+
public function limitHistory($limit)
366+
{
367+
$this->historyLimit = $limit === null ? null : (int)$limit;
368+
369+
// limit send and currently exceeded
370+
if ($this->historyLimit !== null && isset($this->historyLines[$this->historyLimit])) {
371+
// adjust position in history according to new position after applying limit
372+
if ($this->historyPosition !== null) {
373+
$this->historyPosition -= count($this->historyLines) - $this->historyLimit;
374+
375+
// current position will drop off from list => restore original
376+
if ($this->historyPosition < 0) {
377+
$this->setInput($this->historyUnsaved);
378+
$this->historyPosition = null;
379+
$this->historyUnsaved = null;
380+
}
381+
}
382+
383+
$this->historyLines = array_slice($this->historyLines, -$this->historyLimit, $this->historyLimit);
384+
}
325385

326386
return $this;
327387
}
@@ -468,16 +528,40 @@ public function onKeyRight()
468528
/** @internal */
469529
public function onKeyUp()
470530
{
471-
if ($this->history !== null) {
472-
$this->history->up();
531+
// ignore if already at top or history is empty
532+
if ($this->historyPosition === 0 || !$this->historyLines) {
533+
return;
534+
}
535+
536+
if ($this->historyPosition === null) {
537+
// first time up => move to last entry
538+
$this->historyPosition = count($this->historyLines) - 1;
539+
$this->historyUnsaved = $this->getInput();
540+
} else {
541+
// somewhere in the list => move by one
542+
$this->historyPosition--;
473543
}
544+
545+
$this->setInput($this->historyLines[$this->historyPosition]);
474546
}
475547

476548
/** @internal */
477549
public function onKeyDown()
478550
{
479-
if ($this->history !== null) {
480-
$this->history->down();
551+
// ignore if not currently cycling through history
552+
if ($this->historyPosition === null) {
553+
return;
554+
}
555+
556+
if (isset($this->historyLines[$this->historyPosition + 1])) {
557+
// this is still a valid position => advance by one and apply
558+
$this->historyPosition++;
559+
$this->setInput($this->historyLines[$this->historyPosition]);
560+
} else {
561+
// moved beyond bottom => restore original unsaved input
562+
$this->setInput($this->historyUnsaved);
563+
$this->historyPosition = null;
564+
$this->historyUnsaved = null;
481565
}
482566
}
483567

@@ -537,6 +621,10 @@ public function deleteChar($n)
537621
*/
538622
protected function processLine()
539623
{
624+
// reset history cycle position
625+
$this->historyPosition = null;
626+
$this->historyUnsaved = null;
627+
540628
// store and reset/clear/redraw current input
541629
$line = $this->linebuffer;
542630
if ($line !== '') {
@@ -548,9 +636,6 @@ protected function processLine()
548636
}
549637

550638
// process stored input buffer
551-
if ($this->history !== null) {
552-
$this->history->addLine($line);
553-
}
554639
$this->emit('data', array($line));
555640
}
556641

0 commit comments

Comments
 (0)