Skip to content

Commit 2e34486

Browse files
authored
Merge pull request #86 from clue-labs/beep
Emit audible/visible BELL signal when using a disabled function
2 parents 435142a + 7cfb2a1 commit 2e34486

File tree

4 files changed

+153
-26
lines changed

4 files changed

+153
-26
lines changed

src/Readline.php

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Readline extends EventEmitter implements ReadableStreamInterface
2121
private $linepos = 0;
2222
private $echo = true;
2323
private $move = true;
24+
private $bell = true;
2425
private $encoding = 'utf-8';
2526

2627
private $input;
@@ -472,10 +473,7 @@ public function limitHistory($limit)
472473
* @param callable|null $autocomplete
473474
* @return self
474475
* @throws \InvalidArgumentException if the given callable is invalid
475-
<<<<<<< HEAD
476-
=======
477476
* @deprecated use Stdio::setAutocomplete() instead
478-
>>>>>>> Deprecate Readline and move all methods to Stdio
479477
*/
480478
public function setAutocomplete($autocomplete)
481479
{
@@ -488,6 +486,26 @@ public function setAutocomplete($autocomplete)
488486
return $this;
489487
}
490488

489+
/**
490+
* Whether or not to emit a audible/visible BELL signal when using a disabled function
491+
*
492+
* By default, this class will emit a BELL signal when using a disable function,
493+
* such as using the <kbd>left</kbd> or <kbd>backspace</kbd> keys when
494+
* already at the beginning of the line.
495+
*
496+
* Whether or not the BELL is audible/visible depends on the termin and its
497+
* settings, i.e. some terminals may "beep" or flash the screen or emit a
498+
* short vibration.
499+
*
500+
* @param bool $bell
501+
* @return void
502+
* @internal use Stdio::setBell() instead
503+
*/
504+
public function setBell($bell)
505+
{
506+
$this->bell = (bool)$bell;
507+
}
508+
491509
/**
492510
* redraw the current input prompt
493511
*
@@ -537,36 +555,49 @@ public function getDrawString()
537555
public function onKeyBackspace()
538556
{
539557
// left delete only if not at the beginning
540-
$this->deleteChar($this->linepos - 1);
558+
if ($this->linepos === 0) {
559+
$this->bell();
560+
} else {
561+
$this->deleteChar($this->linepos - 1);
562+
}
541563
}
542564

543565
/** @internal */
544566
public function onKeyDelete()
545567
{
546568
// right delete only if not at the end
547-
$this->deleteChar($this->linepos);
569+
if ($this->isEol()) {
570+
$this->bell();
571+
} else {
572+
$this->deleteChar($this->linepos);
573+
}
548574
}
549575

550576
/** @internal */
551577
public function onKeyHome()
552578
{
553-
if ($this->move) {
579+
if ($this->move && $this->linepos !== 0) {
554580
$this->moveCursorTo(0);
581+
} else {
582+
$this->bell();
555583
}
556584
}
557585

558586
/** @internal */
559587
public function onKeyEnd()
560588
{
561-
if ($this->move) {
589+
if ($this->move && !$this->isEol()) {
562590
$this->moveCursorTo($this->strlen($this->linebuffer));
591+
} else {
592+
$this->bell();
563593
}
564594
}
565595

566596
/** @internal */
567597
public function onKeyTab()
568598
{
569599
if ($this->autocomplete === null) {
600+
$this->bell();
570601
return;
571602
}
572603

@@ -616,6 +647,7 @@ public function onKeyTab()
616647

617648
// return if neither of the possible words match
618649
if (!$words) {
650+
$this->bell();
619651
return;
620652
}
621653

@@ -684,16 +716,20 @@ public function onKeyEnter()
684716
/** @internal */
685717
public function onKeyLeft()
686718
{
687-
if ($this->move) {
719+
if ($this->move && $this->linepos !== 0) {
688720
$this->moveCursorBy(-1);
721+
} else {
722+
$this->bell();
689723
}
690724
}
691725

692726
/** @internal */
693727
public function onKeyRight()
694728
{
695-
if ($this->move) {
729+
if ($this->move && !$this->isEol()) {
696730
$this->moveCursorBy(1);
731+
} else {
732+
$this->bell();
697733
}
698734
}
699735

@@ -702,6 +738,7 @@ public function onKeyUp()
702738
{
703739
// ignore if already at top or history is empty
704740
if ($this->historyPosition === 0 || !$this->historyLines) {
741+
$this->bell();
705742
return;
706743
}
707744

@@ -722,6 +759,7 @@ public function onKeyDown()
722759
{
723760
// ignore if not currently cycling through history
724761
if ($this->historyPosition === null) {
762+
$this->bell();
725763
return;
726764
}
727765

@@ -781,18 +819,10 @@ public function onFallback($chars, EventEmitterInterface $base = null)
781819
* Removing a character left to the current cursor will also move the cursor
782820
* to the left.
783821
*
784-
* indices out of range (exceeding current input buffer) are simply ignored
785-
*
786822
* @param int $n
787-
* @internal
788823
*/
789-
public function deleteChar($n)
824+
private function deleteChar($n)
790825
{
791-
$len = $this->strlen($this->linebuffer);
792-
if ($n < 0 || $n >= $len) {
793-
return;
794-
}
795-
796826
// read everything up until before current position
797827
$pre = $this->substr($this->linebuffer, 0, $n);
798828
$post = $this->substr($this->linebuffer, $n + 1);
@@ -911,6 +941,24 @@ private function strsplit($str)
911941
return preg_split('//u', $str, null, PREG_SPLIT_NO_EMPTY);
912942
}
913943

944+
/**
945+
* @return bool
946+
*/
947+
private function isEol()
948+
{
949+
return $this->linepos === $this->strlen($this->linebuffer);
950+
}
951+
952+
/**
953+
* @return void
954+
*/
955+
private function bell()
956+
{
957+
if ($this->bell) {
958+
$this->output->write("\x07"); // BEL a.k.a. \a
959+
}
960+
}
961+
914962
/** @internal */
915963
public function handleEnd()
916964
{

src/Stdio.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,25 @@ public function setAutocomplete($autocomplete)
452452
$this->readline->setAutocomplete($autocomplete);
453453
}
454454

455+
/**
456+
* whether or not to emit a audible/visible BELL signal when using a disabled function
457+
*
458+
* By default, this class will emit a BELL signal when using a disable function,
459+
* such as using the <kbd>left</kbd> or <kbd>backspace</kbd> keys when
460+
* already at the beginning of the line.
461+
*
462+
* Whether or not the BELL is audible/visible depends on the termin and its
463+
* settings, i.e. some terminals may "beep" or flash the screen or emit a
464+
* short vibration.
465+
*
466+
* @param bool $bell
467+
* @return void
468+
*/
469+
public function setBell($bell)
470+
{
471+
$this->readline->setBell($bell);
472+
}
473+
455474
private function width($str)
456475
{
457476
return $this->readline->strwidth($str) - 2 * substr_count($str, "\x08");

tests/ReadlineTest.php

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,27 @@ public function testKeysHomeMovesToFront()
311311
return $this->readline;
312312
}
313313

314+
public function testKeysHomeEmitsBellWhenAlreadyAtBeginningOfLine()
315+
{
316+
$this->output->expects($this->once())->method('write')->with("\x07");
317+
$this->readline->onKeyHome();
318+
}
319+
320+
public function testKeysHomeDoesNotEmitBellWhenAlreadyAtBeginningOfLineButBellIsDisabled()
321+
{
322+
$this->output->expects($this->never())->method('write');
323+
$this->readline->setBell(false);
324+
$this->readline->onKeyHome();
325+
}
326+
327+
public function testKeysHomeEmitsBellWhenAlreadyAtBeginningOfLineAndBellIsEnabledAgain()
328+
{
329+
$this->output->expects($this->once())->method('write')->with("\x07");
330+
$this->readline->setBell(false);
331+
$this->readline->setBell(true);
332+
$this->readline->onKeyHome();
333+
}
334+
314335
/**
315336
* @depends testKeysHomeMovesToFront
316337
* @param Readline $readline
@@ -324,6 +345,12 @@ public function testKeysEndMovesToEnd(Readline $readline)
324345
return $readline;
325346
}
326347

348+
public function testKeysEndEmitsBellWhenAlreadyAtEndOfLine()
349+
{
350+
$this->output->expects($this->once())->method('write')->with("\x07");
351+
$this->readline->onKeyEnd();
352+
}
353+
327354
/**
328355
* @depends testKeysEndMovesToEnd
329356
* @param Readline $readline
@@ -337,6 +364,12 @@ public function testKeysLeftMovesToLeft(Readline $readline)
337364
return $readline;
338365
}
339366

367+
public function testKeysLeftEmitsBellWhenAlreadyAtBeginningOfLine()
368+
{
369+
$this->output->expects($this->once())->method('write')->with("\x07");
370+
$this->readline->onKeyLeft();
371+
}
372+
340373
/**
341374
* @depends testKeysLeftMovesToLeft
342375
* @param Readline $readline
@@ -348,6 +381,12 @@ public function testKeysRightMovesToRight(Readline $readline)
348381
$this->assertEquals(4, $readline->getCursorPosition());
349382
}
350383

384+
public function testKeysRightEmitsBellWhenAlreadyAtEndOfLine()
385+
{
386+
$this->output->expects($this->once())->method('write')->with("\x07");
387+
$this->readline->onKeyRight();
388+
}
389+
351390
public function testKeysSimpleChars()
352391
{
353392
$this->input->emit('data', array('hi!'));
@@ -372,6 +411,12 @@ public function testKeysBackspaceDeletesLastCharacter(Readline $readline)
372411
$this->assertEquals(2, $readline->getCursorCell());
373412
}
374413

414+
public function testKeysBackspaceEmitsBellWhenAlreadyAtBeginningOfLine()
415+
{
416+
$this->output->expects($this->once())->method('write')->with("\x07");
417+
$this->readline->onKeyBackspace();
418+
}
419+
375420
public function testKeysMultiByteInput()
376421
{
377422
$this->input->emit('data', array(''));
@@ -430,10 +475,11 @@ public function testKeysDeleteMiddle()
430475
$this->assertEquals(2, $this->readline->getCursorCell());
431476
}
432477

433-
public function testKeysDeleteEndDoesNothing()
478+
public function testKeysDeleteEmitsBellWhenAlreadyAtEndOfLine()
434479
{
435480
$this->readline->setInput('test');
436481

482+
$this->output->expects($this->once())->method('write')->with("\x07");
437483
$this->readline->onKeyDelete();
438484

439485
$this->assertEquals('test', $this->readline->getInput());
@@ -571,11 +617,9 @@ public function testAutocompleteThrowsIfNotCallable()
571617
$this->assertSame($this->readline, $this->readline->setAutocomplete(123));
572618
}
573619

574-
/**
575-
* @doesNotPerformAssertions
576-
*/
577-
public function testAutocompleteKeyDoesNothingIfUnused()
620+
public function testAutocompleteKeyEmitsBellWhenAutocompleteIsNotSet()
578621
{
622+
$this->output->expects($this->once())->method('write')->with("\x07");
579623
$this->readline->onKeyTab();
580624
}
581625

@@ -784,12 +828,13 @@ public function testAutocompletePicksFirstComplete()
784828
$this->assertEquals('exit ', $this->readline->getInput());
785829
}
786830

787-
public function testAutocompleteIgnoresNonMatching()
831+
public function testAutocompleteIgnoresNonMatchingAndEmitsBell()
788832
{
789833
$this->readline->setAutocomplete(function () { return array('quit'); });
790834

791835
$this->readline->setInput('e');
792836

837+
$this->output->expects($this->once())->method('write')->with("\x07");
793838
$this->readline->onKeyTab();
794839

795840
$this->assertEquals('e', $this->readline->getInput());
@@ -1193,8 +1238,9 @@ public function testHistoryAddEndsUpInList()
11931238
$this->assertEquals(array('a', 'b', 'c'), $this->readline->listHistory());
11941239
}
11951240

1196-
public function testHistoryUpEmptyDoesNotChangeInput()
1241+
public function testHistoryUpEmptyDoesNotChangeInputAndEmitsBell()
11971242
{
1243+
$this->output->expects($this->once())->method('write')->with("\x07");
11981244
$this->readline->onKeyUp();
11991245

12001246
$this->assertEquals('', $this->readline->getInput());
@@ -1236,8 +1282,9 @@ public function testHistoryUpAndThenEnterRestoresCycleToBottom()
12361282
$this->assertEquals('b', $this->readline->getInput());
12371283
}
12381284

1239-
public function testHistoryDownNotCyclingDoesNotChangeInput()
1285+
public function testHistoryDownNotCyclingDoesNotChangeInputAndEmitsBell()
12401286
{
1287+
$this->output->expects($this->once())->method('write')->with("\x07");
12411288
$this->readline->onKeyDown();
12421289

12431290
$this->assertEquals('', $this->readline->getInput());

tests/StdioTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,4 +560,17 @@ public function testHistoryWillBeForwardedToReadline()
560560
$stdio->clearHistory();
561561
$this->assertEquals(array(), $stdio->listHistory());
562562
}
563+
564+
public function testSetBellWillBeForwardedToReadline()
565+
{
566+
$input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock();
567+
$output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock();
568+
$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
569+
570+
$stdio = new Stdio($this->loop, $input, $output, $readline);
571+
572+
$readline->expects($this->once())->method('setBell')->with(false);
573+
574+
$stdio->setBell(false);
575+
}
563576
}

0 commit comments

Comments
 (0)