Skip to content
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ TART ships with a default theme, but you can customize colors, highlight behavio

### Theme Options

- `class` - The theme class to use (defaults to `IGC\Tart\Themes\Theme`)
- `class` - The theme class to use (defaults to `IGC\Tart\Themes\DefaultTheme`)
- `color` - Primary color used by blocks and accents (default: `blue`)
- `text_color` - Default text color inside themed blocks (default: `white`)
- `highlight_color` - Accent/highlight color (default: `yellow`)
Expand Down Expand Up @@ -263,13 +263,13 @@ Need manual control? Add TART to Composer's `dont-discover` list and register th

#### Publish Configuration

Publish the default configuration to tweak the base theme, logo colors, or auto-answer behavior:
Publish the default configuration to tweak the base theme, logo colors, auto-answer behavior, and demo-command registration:

```bash
php artisan vendor:publish --tag=tart-config
```

`config/tart.php` lets you point to a custom `ThemeInterface` implementation or adjust the palette/width used by the bundled `Theme` class.
`config/tart.php` lets you point to a custom `ThemeInterface` implementation, adjust palette/width used by the bundled `Theme` class, and control `register_demo_commands` (defaults to `false`).

## 📖 Core Features

Expand Down
12 changes: 12 additions & 0 deletions config/tart.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
*/
'auto_answer' => false,


/*
|--------------------------------------------------------------------------
| Demo Command Registration
|--------------------------------------------------------------------------
|
| Controls whether package demo commands are registered with Artisan.
| Disable in production to keep your command namespace clean.
|
*/
'register_demo_commands' => false,

/*
|--------------------------------------------------------------------------
| Theme Defaults
Expand Down
12 changes: 8 additions & 4 deletions src/Concerns/HasEnhancedInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Closure;
use IGC\Tart\Support\FrameRenderer;
use IGC\Tart\Support\TerminalMode;

trait HasEnhancedInput
{
Expand Down Expand Up @@ -90,13 +91,16 @@ public function password(string $question): string
*/
protected function getHiddenInputUnix(): string
{
$sttyMode = shell_exec('stty -g');
$terminal = new TerminalMode();

$terminal->enableRawMode();
shell_exec('stty -echo');
$answer = trim((string) fgets(STDIN));
shell_exec(sprintf('stty %s', $sttyMode));

return $answer;
try {
return trim((string) fgets(STDIN));
} finally {
$terminal->restore();
}
}

/**
Expand Down
28 changes: 20 additions & 8 deletions src/Laravel/TartServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,26 @@ public function boot(): void
self::CONFIG_PATH => $this->configPath(),
], 'tart-config');

$this->commands([
Commands\TartDemoCommand::class,
Commands\TartDemoFullCommand::class,
Commands\TartFluentDemoCommand::class,
Commands\TartTestCommand::class,
Commands\TartNewFeaturesDemo::class,
Commands\TartInteractiveDemoCommand::class,
]);
if ($this->shouldRegisterDemoCommands()) {
$this->commands([
Commands\TartDemoCommand::class,
Commands\TartDemoFullCommand::class,
Commands\TartFluentDemoCommand::class,
Commands\TartTestCommand::class,
Commands\TartNewFeaturesDemo::class,
Commands\TartInteractiveDemoCommand::class,
]);
}
}

protected function shouldRegisterDemoCommands(): bool
{
$configured = $this->app['config']->get('tart.register_demo_commands');
if ($configured !== null) {
return (bool) $configured;
}

return $this->app->environment(['local', 'testing']);
}

protected function configPath(): string
Expand Down
58 changes: 33 additions & 25 deletions src/Support/MultiSelect.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,47 +117,55 @@ protected function render(array $keys, array $values): void
*/
protected function readInput(array $keys, array $values): array
{
$sttyMode = shell_exec('stty -g');
$terminal = new TerminalMode();
$input = new TerminalInput();

system('stty -icanon -echo');
$terminal->enableRawMode();

while (true) {
$char = fread(STDIN, 1);
try {
while (true) {
$keyPress = $input->readKey();

if ($char === "\n") {
if ($this->minRequired > 0 && count($this->selected) < $this->minRequired) {
if ($keyPress === null) {
continue;
}
break;
}

if ($char === ' ') {
$key = $keys[$this->cursorIndex];
$index = array_search($key, $this->selected, true);
if ($keyPress->type === KeyPress::ENTER) {
if ($this->minRequired > 0 && count($this->selected) < $this->minRequired) {
continue;
}

if ($index !== false) {
unset($this->selected[$index]);
$this->selected = array_values($this->selected);
} else {
$this->selected[] = $key;
break;
}

$this->updateDisplay($keys, $values);
} elseif ($char === "\033") {
$arrow = fread(STDIN, 2);
if ($keyPress->type === KeyPress::SPACE) {
$key = $keys[$this->cursorIndex];
$index = array_search($key, $this->selected, true);

if ($index !== false) {
unset($this->selected[$index]);
$this->selected = array_values($this->selected);
} else {
$this->selected[] = $key;
}

$this->updateDisplay($keys, $values);
continue;
}

if ($arrow === '[A') {
if ($keyPress->type === KeyPress::UP) {
$this->cursorIndex = max(0, $this->cursorIndex - 1);
$this->updateDisplay($keys, $values);
} elseif ($arrow === '[B') {
continue;
}

if ($keyPress->type === KeyPress::DOWN) {
$this->cursorIndex = min(count($this->options) - 1, $this->cursorIndex + 1);
$this->updateDisplay($keys, $values);
}
}
}

if ($sttyMode !== null && $sttyMode !== false) {
system("stty {$sttyMode}");
} finally {
$terminal->restore();
}

return $this->selected;
Expand Down
50 changes: 29 additions & 21 deletions src/Support/Select.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use IGC\Tart\Contracts\ThemeInterface;
use IGC\Tart\Themes\DefaultTheme;
use RuntimeException;
use Symfony\Component\Console\Output\OutputInterface;

class Select
Expand Down Expand Up @@ -56,6 +57,10 @@ public function setRequired(bool $required): self
public function ask(): ?string
{
if (empty($this->options)) {
if ($this->required) {
throw new RuntimeException('Selection is required but no options were provided.');
}

return null;
}

Expand Down Expand Up @@ -105,42 +110,45 @@ protected function render(array $keys, array $values): void
*/
protected function readInput(array $keys): ?string
{
$sttyMode = shell_exec('stty -g');
$terminal = new TerminalMode();
$input = new TerminalInput();

system('stty -icanon -echo');
$terminal->enableRawMode();

while (true) {
$char = fread(STDIN, 3);

if ($char === "\n") {
break;
}

if ($char === "\033") {
$arrow = fread(STDIN, 2);
try {
while (true) {
$key = $input->readKey();
if ($key === null) {
continue;
}

if ($arrow === '[A') {
if ($key->type === KeyPress::UP) {
$this->selectedIndex = max(0, $this->selectedIndex - 1);
$this->updateDisplay($keys, array_values($this->options));
} elseif ($arrow === '[B') {
$this->updateDisplay(array_values($this->options));
continue;
}

if ($key->type === KeyPress::DOWN) {
$this->selectedIndex = min(count($this->options) - 1, $this->selectedIndex + 1);
$this->updateDisplay($keys, array_values($this->options));
$this->updateDisplay(array_values($this->options));
continue;
}
}
}

if ($sttyMode !== null && $sttyMode !== false) {
system("stty {$sttyMode}");
if ($key->type === KeyPress::ENTER || $key->type === KeyPress::ESCAPE) {
break;
}
}
} finally {
$terminal->restore();
}

return $keys[$this->selectedIndex] ?? null;
}

/**
* @param array<string> $keys
* @param array<string> $values
*/
protected function updateDisplay(array $keys, array $values): void
protected function updateDisplay(array $values): void
{
$this->output->write(TerminalControl::moveUp(count($this->options)));

Expand Down
13 changes: 12 additions & 1 deletion tests/Integration/TartServiceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,19 @@ protected function getPackageProviders($app)
];
}

public function test_demo_command_is_available(): void
public function test_demo_command_is_available_when_enabled(): void
{
$this->app['config']->set('tart.register_demo_commands', true);

$this->artisan('tart:demo')->assertExitCode(0);
}

public function test_demo_command_is_not_registered_when_disabled(): void
{
$this->app['config']->set('tart.register_demo_commands', false);

$this->artisan('tart:demo')
->expectsOutputToContain('Command "tart:demo" is not defined')
->assertExitCode(1);
}
}
13 changes: 13 additions & 0 deletions tests/Unit/SelectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use IGC\Tart\Support\Select;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Symfony\Component\Console\Output\BufferedOutput;

class SelectTest extends TestCase
Expand Down Expand Up @@ -49,6 +50,18 @@ public function test_select_returns_null_for_empty_options(): void
$this->assertNull($result);
}


public function test_select_required_throws_when_options_are_empty(): void
{
$this->expectException(RuntimeException::class);

$output = new BufferedOutput();

$select = new Select($output, 'Choose option', []);
$select->setRequired(true);
$select->ask();
}

public function test_select_can_set_required(): void
{
$output = new BufferedOutput();
Expand Down
Loading