Skip to content
Merged

Dev #33

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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ test/*
tests/clover.xml
cache/commands.json
*.Identifier
/test2
/home/ibrahim/cli/test2
8 changes: 6 additions & 2 deletions WebFiori/Cli/Commands/InitAppCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ public function exec(): int {
}

if (defined('ROOT_DIR')) {
$appPath = ROOT_DIR.DIRECTORY_SEPARATOR.$dirName;
$path = ROOT_DIR;
if ($path[strlen($path) - 1] != DIRECTORY_SEPARATOR) {
$path .= DIRECTORY_SEPARATOR;
}
$appPath = $path.$dirName;
} else {
$appPath = getcwd().DIRECTORY_SEPARATOR.$dirName;
}
Expand All @@ -44,7 +48,7 @@ public function exec(): int {
$this->success('App created successfully.');

return 0;
} catch (\Exception $ex) {
} catch (\Throwable $ex) {
$this->error('Unable to initialize due to an exception:');
$this->println($ex->getCode().' - '.$ex->getMessage());

Expand Down
37 changes: 29 additions & 8 deletions WebFiori/Cli/KeysMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,31 @@ public static function read(InputStream $stream, $bytes = 1) : string {
*/
public static function readAndTranslate(InputStream $stream) : string {
$keypress = $stream->read();

// Handle EOF
if ($keypress === '') {
return '';
}

// Handle escape sequences (multi-byte) - kept for future use
if ($keypress === "\033") {
try {
// Read the next character to see if it's part of an escape sequence
$next = $stream->read();
if ($next === '[') {
// Read the final character of the sequence
$final = $stream->read();
$sequence = $keypress . $next . $final;
return self::map($sequence);
} else {
// Not a complete escape sequence, return ESC
return self::map($keypress);
}
} catch (\Exception $e) {
// If we can't read more bytes, just return ESC
return self::map($keypress);
}
}

return self::map($keypress);
}
Expand Down Expand Up @@ -134,18 +159,14 @@ private static function appendChar($ch, &$input) {
if ($ch == 'BACKSPACE' && strlen($input) > 0) {
$input = substr($input, 0, strlen($input) - 1);
} else if ($ch == 'ESC') {
$input .= "\e";
// Ignore ESC key
} else if ($ch == "CR") {
// Do nothing - don't add CR to input
} else if ($ch == "LF") {
// Do nothing - don't add LF to input (readLine should not include line ending)
} else if ($ch == 'DOWN') {
// read history;
//$input .= ' ';
} else if ($ch == 'UP') {
// read history;
//$input .= ' ';
} else if ($ch != 'CR' && $ch != 'LF') {
} else if ($ch == 'DOWN' || $ch == 'UP' || $ch == 'LEFT' || $ch == 'RIGHT') {
// Ignore arrow keys - kept for future navigation features
} else {
if ($ch == 'SPACE') {
$input .= ' ';
} else {
Expand Down
2 changes: 1 addition & 1 deletion tests/WebFiori/Tests/Cli/AliasingIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function testAliasingWithHelpCommand() {
$runner->register($aliasCommand);

// Test help for command via direct name (not alias, as help might not resolve aliases)
$runner->setArgsVector(['script.php', 'help', '--command-name=alias-test']);
$runner->setArgsVector(['script.php', 'help', '--command=alias-test']);
$exitCode = $runner->start();

$output = $runner->getOutputStream()->getOutputArray();
Expand Down
43 changes: 35 additions & 8 deletions tests/WebFiori/Tests/Cli/InitAppCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ public function test01() {
'--dir' => "test\0a"
]);
$this->assertEquals(-1, $r->start());
$output = $r->getOutput();
// Check key elements instead of exact match due to binary string representation differences
$this->assertCount(4, $output);
$this->assertStringContainsString('Creating new app at', $output[0]);
$this->assertStringContainsString('Creating "test', $output[1]);
$this->assertStringContainsString('Error: Unable to initialize', $output[2]);
$this->assertStringContainsString('null bytes', $output[3]);
}
/**
* @test
* @depends test01
*/
public function test02() {
$r = new Runner();
Expand All @@ -57,12 +63,20 @@ public function test02() {
'init',
'--dir' => 'test'
]);
$appPath = ROOT_DIR.DS.'test';
// Cleanup existing files
$appPath = ROOT_DIR.'test';
if (file_exists($appPath)) {
unlink(ROOT_DIR.DS.'test'.DS.'main.php');
unlink(ROOT_DIR.DS.'test'.DS.'HelloCommand.php');
unlink(ROOT_DIR.DS.'test'.DS.'main');
rmdir(ROOT_DIR.DS.'test');
}
$this->assertEquals(0, $r->start());
$this->assertEquals([
"Creating new app at \"$appPath\" ...\n",
"Creating \"test/main.php\"...\n",
"Creating \"test/test\"...\n",
"Creating \"test/main\"...\n",
"Creating \"test/HelloCommand.php\"...\n",
"Success: App created successfully.\n"
], $r->getOutput());
}
Expand All @@ -80,18 +94,22 @@ public function test03() {
'init',
'--dir' => 'test'
]);
// Don't cleanup - this test expects files to exist
$this->assertEquals(0, $r->start());
$appPath = ROOT_DIR.DS.'test';
$appPath = ROOT_DIR.'test';
$this->assertEquals([
"Creating new app at \"$appPath\" ...\n",
"Creating \"test/main.php\"...\n",
"Warning: File main.php already exist!\n",
"Creating \"test/test\"...\n",
"Warning: File test already exist!\n",
"Creating \"test/main\"...\n",
"Warning: File main already exist!\n",
"Creating \"test/HelloCommand.php\"...\n",
"Warning: File HelloCommand.php already exist!\n",
"Success: App created successfully.\n"
], $r->getOutput());
unlink(ROOT_DIR.DS.'test'.DS.'main.php');
unlink(ROOT_DIR.DS.'test'.DS.'test');
unlink(ROOT_DIR.DS.'test'.DS.'HelloCommand.php');
unlink(ROOT_DIR.DS.'test'.DS.'main');
rmdir(ROOT_DIR.DS.'test');
}
/**
Expand All @@ -108,16 +126,25 @@ public function test04() {
'--dir' => 'test2',
'--entry' => 'bang'
]);
// Cleanup existing files
$appPath = ROOT_DIR.'test2';
if (file_exists($appPath)) {
unlink($appPath.DS.'main.php');
unlink($appPath.DS.'bang');
unlink($appPath.DS.'HelloCommand.php');
rmdir($appPath);
}
$this->assertEquals(0, $r->start());
$appPath = ROOT_DIR.DS.'test2';
$this->assertEquals([
"Creating new app at \"$appPath\" ...\n",
"Creating \"test2/main.php\"...\n",
"Creating \"test2/bang\"...\n",
"Creating \"test2/HelloCommand.php\"...\n",
"Success: App created successfully.\n"
], $r->getOutput());
unlink($appPath.DS.'main.php');
unlink($appPath.DS.'bang');
unlink($appPath.DS.'HelloCommand.php');
rmdir($appPath);
}
}
Expand Down
84 changes: 61 additions & 23 deletions tests/WebFiori/Tests/Cli/KeysMapTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,77 @@
use PHPUnit\Framework\TestCase;
use WebFiori\Cli\KeysMap;
use WebFiori\Cli\Streams\ArrayInputStream;

/**
* Description of KeysMapTest
*
* @author Ibrahim
* Test cases for KeysMap class arrow key handling.
*/
class KeysMapTest extends TestCase {

/**
* Test that arrow key escape sequences are properly detected.
*/
public function testArrowKeyDetection() {
// Test UP arrow
$inputStream = new ArrayInputStream(["\033[A"]);
$result = KeysMap::readAndTranslate($inputStream);
$this->assertEquals('UP', $result);

// Test DOWN arrow
$inputStream = new ArrayInputStream(["\033[B"]);
$result = KeysMap::readAndTranslate($inputStream);
$this->assertEquals('DOWN', $result);

// Test RIGHT arrow
$inputStream = new ArrayInputStream(["\033[C"]);
$result = KeysMap::readAndTranslate($inputStream);
$this->assertEquals('RIGHT', $result);

// Test LEFT arrow
$inputStream = new ArrayInputStream(["\033[D"]);
$result = KeysMap::readAndTranslate($inputStream);
$this->assertEquals('LEFT', $result);
}

/**
* Test that arrow keys don't appear in readline input.
*/
public function testArrowKeysIgnoredInReadLine() {
// Input with arrow keys mixed with regular text
$inputStream = new ArrayInputStream(["\033[A\033[Bhello\033[C\033[D\n"]);
$result = KeysMap::readLine($inputStream);

// Should only contain "hello", arrow keys should be ignored
$this->assertEquals('hello', $result);
}

/**
* @test
* Test that regular characters still work normally.
*/
public function test00() {
$stream = new ArrayInputStream([
chr(27) // ESC character
]);
$this->assertEquals("ESC", KeysMap::readAndTranslate($stream));
public function testRegularCharacters() {
$inputStream = new ArrayInputStream(["hello world\n"]);
$result = KeysMap::readLine($inputStream);

$this->assertEquals('hello world', $result);
}

/**
* @test
* Test backspace functionality still works.
*/
public function test01() {
$stream = new ArrayInputStream([
"\r"
]);
$this->assertEquals("CR", KeysMap::readAndTranslate($stream));
public function testBackspaceWithArrowKeys() {
// Type "hello", backspace once, arrow keys (ignored), type "p"
$inputStream = new ArrayInputStream(["hello\177\033[A\033[Bp\n"]);
$result = KeysMap::readLine($inputStream);

// Should be "hellp" (hello -> hell -> hell -> hell -> hellp)
$this->assertEquals('hellp', $result);
}

/**
* @test
* Test ESC key handling.
*/
public function test02() {
$stream = new ArrayInputStream([
"\r",
"\n"
]);
$this->assertEquals("CR", KeysMap::readAndTranslate($stream));
$this->assertEquals("LF", KeysMap::readAndTranslate($stream));
public function testEscapeKey() {
$inputStream = new ArrayInputStream(["\ehello\n"]);
$result = KeysMap::readAndTranslate($inputStream);
$this->assertEquals('ESC', $result);
}
}
Loading