Skip to content

Commit

Permalink
Merge pull request #9 from aarondfrancis/af-marquee
Browse files Browse the repository at this point in the history
Marquee long processes. Add ANSI aware helpers
  • Loading branch information
aarondfrancis authored Nov 8, 2024
2 parents 6d6fef4 + 8951a14 commit 15af7ef
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 14 deletions.
11 changes: 4 additions & 7 deletions pint.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@
"function_declaration": {
"closure_fn_spacing": "none"
},
"trailing_comma_in_multiline": false,
"header_comment": {
"header": "@author Aaron Francis <[email protected]>\n@link https://aaronfrancis.com\n@link https://twitter.com/aarondfrancis",
"comment_type": "PHPDoc",
"location": "after_open",
"separate": "bottom"
}
"phpdoc_separation": {
"groups": [["author","link"],["param","return"]]
},
"trailing_comma_in_multiline": false
}
}
2 changes: 1 addition & 1 deletion src/Console/Commands/About.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function handle()
• Twitter: https://twitter.com/aarondfrancis
• Website: https://aaronfrancis.com
• YouTube: https://youtube.com/@aarondfrancis
GitHub: https://github.com/aarondfrancis/solo
• GitHub: https://github.com/aarondfrancis/solo
EOT;
Expand Down
124 changes: 124 additions & 0 deletions src/Helpers/AnsiAware.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,130 @@

class AnsiAware
{
public static function mb_strlen($string): int
{
// Regular expression to match ANSI escape sequences
$ansiEscapeSequence = '/\x1b\[[0-9;]*[A-Za-z]/';

// Remove ANSI escape sequences
$plainString = preg_replace($ansiEscapeSequence, '', $string);

// Return length of the plain string
return mb_strlen($plainString);
}

public static function substr($string, $start, $length = null): string
{
$ansiEscapeSequence = '/(\x1b\[[0-9;]*[mGK])/';
$parts = preg_split($ansiEscapeSequence, $string, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);

$currentPos = 0; // Position in printable characters.
$substringParts = []; // Parts of the substring.
$openAnsiCodes = []; // Array of open ANSI codes.
$collecting = false; // Whether we are collecting substring parts.
$startPos = $start; // Starting position.

// If $length is null, we need to go till the end of the string.
$endPos = is_null($length) ? PHP_INT_MAX : $start + $length;

foreach ($parts as $part) {
if (preg_match($ansiEscapeSequence, $part)) {
// It's an ANSI code.
// Update $openAnsiCodes accordingly.
if (str_contains($part, 'm')) {
// It's an SGR code.
$sgrParams = substr($part, 2, -1); // Remove "\e[" and "m"
$sgrCodes = explode(';', $sgrParams);

foreach ($sgrCodes as $code) {
$code = intval($code);
if ($code == 0) {
// Reset all attributes.
$openAnsiCodes = [];
} else {
if (($code >= 30 && $code <= 37) || ($code >= 90 && $code <= 97)) {
// Set foreground color.
// Remove any existing foreground color codes.
$openAnsiCodes = array_filter($openAnsiCodes, function ($c) {
return !(($c >= 30 && $c <= 37) || ($c >= 90 && $c <= 97));
});
} else {
if (($code >= 40 && $code <= 47) || ($code >= 100 && $code <= 107)) {
// Set background color.
// Remove any existing background color codes.
$openAnsiCodes = array_filter($openAnsiCodes, function ($c) {
return !(($c >= 40 && $c <= 47) || ($c >= 100 && $c <= 107));
});
}
}
$openAnsiCodes[] = $code;
}
}
}
// If we are collecting, we need to include this ANSI code.
if ($collecting) {
$substringParts[] = $part;
}

continue;
}

// It's a printable text part.
$partLength = mb_strlen($part);

if ($currentPos + $partLength <= $startPos) {
// This part is entirely before the start position.
$currentPos += $partLength;

continue;
}

if ($currentPos >= $endPos) {
// We have already reached or passed the end position.
break;
}

// Now, part of this $part is within the desired range.
$partStart = 0;
$partEnd = $partLength;

if ($currentPos < $startPos) {
// The desired start position is within this part.
$partStart = $startPos - $currentPos;
}

if ($currentPos + $partLength > $endPos) {
// The desired end position is within this part.
$partEnd = $endPos - $currentPos;
}

// Extract the substring from this part.
$substring = mb_substr($part, $partStart, $partEnd - $partStart);

// If we are just starting to collect, prepend open ANSI codes.
if (!$collecting) {
$collecting = true;
if (!empty($openAnsiCodes)) {
// Build the ANSI code string.
$ansiCodeString = "\e[" . implode(';', $openAnsiCodes) . 'm';
$substringParts[] = $ansiCodeString;
}
}

$substringParts[] = $substring;

$currentPos += $partLength;
}

// Close any open ANSI codes.
if ($collecting && !empty($openAnsiCodes)) {
// Append reset code.
$substringParts[] = "\e[0m";
}

return implode('', $substringParts);
}

public static function wordwrap($string, $width = 75, $break = PHP_EOL, $cut = false): string
{
$ansiEscapeSequence = '/(\x1b\[[0-9;]*[mGK])/';
Expand Down
6 changes: 6 additions & 0 deletions src/Prompt/Dashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use AaronFrancis\Solo\Commands\Command;
use AaronFrancis\Solo\Facades\Solo;
use AaronFrancis\Solo\Support\Frames;
use Chewie\Concerns\CreatesAnAltScreen;
use Chewie\Concerns\Loops;
use Chewie\Concerns\RegistersRenderers;
Expand All @@ -34,6 +35,8 @@ class Dashboard extends Prompt

public int $height;

public Frames $frames;

public static function start(): void
{
(new static)->run();
Expand All @@ -48,6 +51,8 @@ public function __construct()

pcntl_signal(SIGWINCH, [$this, 'handleResize']);

$this->frames = new Frames;

$this->commands = collect(Solo::commands())->each(function (Command $command) {
$command->setDimensions($this->width, $this->height);
$command->autostart();
Expand Down Expand Up @@ -143,6 +148,7 @@ protected function showDashboard(): void
$this->render();

$listener->once();
$this->frames->next();
}, 25_000);

// @TODO reconsider using?
Expand Down
38 changes: 36 additions & 2 deletions src/Prompt/Renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use AaronFrancis\Solo\Commands\Command;
use AaronFrancis\Solo\Contracts\Theme;
use AaronFrancis\Solo\Facades\Solo;
use AaronFrancis\Solo\Helpers\AnsiAware;
use Chewie\Concerns\Aligns;
use Chewie\Concerns\DrawsHotkeys;
use Chewie\Output\Util;
Expand Down Expand Up @@ -139,9 +140,42 @@ protected function renderProcessState(): void
? $this->theme->processRunning(' Running: ')
: $this->theme->processStopped(' Stopped: ');

$state .= $this->theme->dim($this->currentCommand->command);
$command = $this->marquee(
$this->dim($this->currentCommand->command),
$this->width - AnsiAware::mb_strlen($state),
$this->dashboard->frames->current(buffer: 6)
);

$this->line($state . $command);
}

protected function marquee(string $string, int $width, int $frame)
{
$length = AnsiAware::mb_strlen($string);

if ($length <= $width) {
return $string;
}

// Maximum starting position
$maxPos = $length - $width;

// Define the sequence of positions for the marquee effect
$starts = array_merge(
[0, 0], // Pause at start for one frame
range(1, $maxPos), // Move forward
[$maxPos, $maxPos], // Pause at end for one frame
range($maxPos - 1, 0, -1), // Move backward
[0] // Pause at start again before repeating
);

$totalFrames = count($starts);

// Calculate the current index in the positions array
$index = $frame % $totalFrames;
$start = $starts[$index];

$this->line($state);
return AnsiAware::substr($string, $start, $width);
}

protected function renderContentPane(): void
Expand Down
8 changes: 4 additions & 4 deletions src/Stubs/SoloServiceProvider.stub
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ class SoloServiceProvider extends SoloApplicationServiceProvider
public function register()
{
Solo::useTheme('dark')
// FQCNs of trusted classes that can add commands.
->allowCommandsAddedFrom([
//
])
// Commands that auto start.
->addCommands([
EnhancedTailCommand::make('Logs', 'tail -f -n 100 ' . storage_path('logs/laravel.log')),
Expand All @@ -27,6 +23,10 @@ class SoloServiceProvider extends SoloApplicationServiceProvider
'Queue' => 'php artisan queue:listen --tries=1',
// 'Reverb' => 'php artisan reverb:start',
// 'Pint' => 'pint --ansi',
])
// FQCNs of trusted classes that can add commands.
->allowCommandsAddedFrom([
//
]);
}

Expand Down
30 changes: 30 additions & 0 deletions src/Support/Frames.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* @author Aaron Francis <[email protected]>
* @link https://aaronfrancis.com
* @link https://twitter.com/aarondfrancis
*/

declare(strict_types=1);

namespace AaronFrancis\Solo\Support;

class Frames
{
protected int $current = 0;

public function next()
{
$this->current++;
}

public function current($buffer = 1)
{
return floor($this->current / $buffer);
}

public function frame(array $frames, $buffer = 1)
{
return $frames[$this->current($buffer) % count($frames)];
}
}
79 changes: 79 additions & 0 deletions tests/Unit/AnsiAwareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,85 @@ class AnsiAwareTest extends Base
{
use Colors;

#[Test]
public function ansi_mb_strlen(): void
{
$string = 'Hello ' . $this->green('World!');
$this->assertEquals(12, AnsiAware::mb_strlen($string));

$string = 'Hello ' . $this->bgRed($this->green('World!'));
$this->assertEquals(12, AnsiAware::mb_strlen($string));

$string = 'Hello ' . $this->bold($this->bgRed($this->green('World!')));
$this->assertEquals(12, AnsiAware::mb_strlen($string));

$string = 'Hello ' . $this->bold($this->bgRed($this->green('█orld!')));
$this->assertEquals(12, AnsiAware::mb_strlen($string));
}

#[Test]
public function ansi_substr_foreground(): void
{
$string = 'Hello ' . $this->green('World!');
$substring = AnsiAware::substr($string, 3, 5);

$this->assertEquals("lo \e[32mWo\e[39m\e[0m", $substring);
}

#[Test]
public function ansi_substr_background(): void
{
$string = 'Hello ' . $this->bgGreen('World!');
$substring = AnsiAware::substr($string, 3, 5);

$this->assertEquals("lo \e[42mWo\e[49m\e[0m", $substring);
}

#[Test]
public function ansi_substr_fore_and_background(): void
{
$string = 'Hello ' . $this->bgGreen($this->white('World!'));
$substring = AnsiAware::substr($string, 3, 5);

$this->assertEquals("lo \e[42m\e[37mWo\e[39m\e[49m\e[0m", $substring);
}

#[Test]
public function ansi_substr_bold(): void
{
$string = 'Hello ' . $this->bold('World!');
$substring = AnsiAware::substr($string, 3, 5);

$this->assertEquals("lo \e[1mWo\e[22m\e[0m", $substring);
}

#[Test]
public function ansi_substr_overlap(): void
{
$string = 'Hell' . $this->bgGreen('o ' . $this->blue('World!'));
$substring = AnsiAware::substr($string, 3, 5);

$this->assertEquals("l\e[42mo \e[34mWo\e[39m\e[49m\e[0m", $substring);
}

#[Test]
public function ansi_substr_overlap_multibyte(): void
{
$string = 'Hell' . $this->bgGreen('' . $this->blue('World!'));
$substring = AnsiAware::substr($string, 3, 5);

$this->assertEquals("l\e[42m█ \e[34mWo\e[39m\e[49m\e[0m", $substring);
}

#[Test]
public function ansi_substr_no_style(): void
{
$string = 'Hello World';
$substring = AnsiAware::substr($string, 3, 5);

$this->assertEquals('lo Wo', $substring);
}

#[Test]
public function it_wraps_a_basic_line(): void
{
Expand Down

0 comments on commit 15af7ef

Please sign in to comment.