337 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			337 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
namespace NoccyLabs\JEdit\Editor;
 | 
						|
 | 
						|
use NoccyLabs\JEdit\List\TreeList;
 | 
						|
use NoccyLabs\JEdit\Terminal\Terminal;
 | 
						|
use NoccyLabs\JEdit\Tree\ArrayNode;
 | 
						|
use NoccyLabs\JEdit\Tree\ObjectNode;
 | 
						|
use NoccyLabs\JEdit\Tree\Tree;
 | 
						|
use NoccyLabs\JEdit\Tree\ValueNode;
 | 
						|
use Symfony\Component\Yaml\Yaml;
 | 
						|
 | 
						|
class Editor
 | 
						|
{
 | 
						|
 | 
						|
    private Tree $document;
 | 
						|
 | 
						|
    private TreeList $list;
 | 
						|
 | 
						|
    private int $currentRow = 0;
 | 
						|
 | 
						|
    private int $scrollOffset = 0;
 | 
						|
 | 
						|
    private ?string $filename = "untitled.json";
 | 
						|
 | 
						|
    private ?string $shortfilename = "untitled.json";
 | 
						|
 | 
						|
    public function __construct(private Terminal $term)
 | 
						|
    {
 | 
						|
        $this->document = new Tree();
 | 
						|
 | 
						|
        // $this->document->load((object)[
 | 
						|
        //     'foo' => true,
 | 
						|
        //     'bar' => [
 | 
						|
        //         'a', 'b', 'c'
 | 
						|
        //     ],
 | 
						|
        //     'what' => 42
 | 
						|
        // ]);
 | 
						|
 | 
						|
        $this->list = new TreeList($this->document);
 | 
						|
    }
 | 
						|
 | 
						|
    public function loadFile(string $filename): void
 | 
						|
    {
 | 
						|
        $this->filename = $filename;
 | 
						|
        $this->shortfilename = basename($filename);
 | 
						|
        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
 | 
						|
        switch ($ext) {
 | 
						|
            case 'json':
 | 
						|
                $doc = json_decode(file_get_contents($filename));
 | 
						|
                break;
 | 
						|
            case 'yml':
 | 
						|
            case 'yaml':
 | 
						|
                $doc = Yaml::parseFile($filename);
 | 
						|
                break;    
 | 
						|
            default:
 | 
						|
                throw new \RuntimeException("Unable to read file of type {$ext}");
 | 
						|
        }
 | 
						|
        $this->document->load($doc);
 | 
						|
        $this->list->parseTree();
 | 
						|
    }
 | 
						|
 | 
						|
    public function loadDocument(mixed $document): void
 | 
						|
    {
 | 
						|
        $this->document->load($document);
 | 
						|
        $this->list->parseTree();
 | 
						|
    }
 | 
						|
 | 
						|
    private bool $running = true;
 | 
						|
 | 
						|
    public function run()
 | 
						|
    {
 | 
						|
        $this->list->parseTree();
 | 
						|
        $this->redrawEditor();
 | 
						|
 | 
						|
        while ($this->running) {
 | 
						|
            [$w,$h] = $this->term->getSize();
 | 
						|
            $read = $this->term->readKey();
 | 
						|
            switch ($read) {
 | 
						|
                case 'k{UP}':
 | 
						|
                    $this->currentRow = max(0, $this->currentRow - 1);
 | 
						|
                    $this->redrawEditor();
 | 
						|
                    break;
 | 
						|
                case 'k{DOWN}':
 | 
						|
                    $this->currentRow = min(count($this->list), $this->currentRow + 1);
 | 
						|
                    $this->redrawEditor();
 | 
						|
                    break;
 | 
						|
                case 'k{PGUP}':
 | 
						|
                    $this->currentRow = max(0, $this->currentRow - ($h - 3));
 | 
						|
                    $this->redrawEditor();
 | 
						|
                    break;
 | 
						|
                case 'k{PGDN}':
 | 
						|
                    $this->currentRow = min(count($this->list), $this->currentRow + ($h - 3));
 | 
						|
                    $this->redrawEditor();
 | 
						|
                    break;
 | 
						|
 | 
						|
                case 'E':
 | 
						|
                    //$coll = $this->list->findNearestCollection($this->currentRow);
 | 
						|
                    $entry = $this->list->getEntryForIndex($this->currentRow);
 | 
						|
                    $path = $this->list->getPathForIndex($this->currentRow);
 | 
						|
                    $parentIndex = $this->list->getIndexForPath(dirname($path));
 | 
						|
                    $parent = $this->list->getEntryForIndex($parentIndex);
 | 
						|
                    
 | 
						|
                    if ($parent->node instanceof ObjectNode) {
 | 
						|
                        $newVal = $this->ask("\e[33;1mnew key:\e[0m ", $entry->key);
 | 
						|
                        if ($newVal !== null) {
 | 
						|
                            $entry->key = $newVal;
 | 
						|
                            $this->redrawEditor();
 | 
						|
                        }
 | 
						|
                    } else {
 | 
						|
                        $this->term->setCursor(1, $h);
 | 
						|
                        echo "\e[97;41mCan only edit keys on objects\e[K\e[0m";
 | 
						|
                    }
 | 
						|
                    break;
 | 
						|
    
 | 
						|
                case 'e':
 | 
						|
                    //$coll = $this->list->findNearestCollection($this->currentRow);
 | 
						|
                    $node = $this->list->getNodeForIndex($this->currentRow);
 | 
						|
                    if ($node instanceof ValueNode) {
 | 
						|
                        $val = json_encode($node->value, JSON_UNESCAPED_SLASHES);
 | 
						|
                        $newVal = $this->ask("\e[33;1mnew value:\e[0m ", $val);
 | 
						|
                        if ($newVal !== null) {
 | 
						|
                            $val = json_decode($newVal);
 | 
						|
                            $node->value = $val;
 | 
						|
                            $this->redrawEditor();
 | 
						|
                        } else {
 | 
						|
                            $this->redrawInfoBar();
 | 
						|
                        }
 | 
						|
                    } else {
 | 
						|
                        $this->term->setCursor(1, $h);
 | 
						|
                        echo "\e[97;41mCan not edit array/object\e[K\e[0m";
 | 
						|
                    }
 | 
						|
                    break;
 | 
						|
 | 
						|
                case '[':
 | 
						|
                    break;                    
 | 
						|
 | 
						|
                case 'D':
 | 
						|
                    $node = $this->list->getNodeForIndex($this->currentRow);
 | 
						|
                    $path = $this->list->getPathForIndex($this->currentRow);
 | 
						|
                    $parentPath = dirname($path);
 | 
						|
                    $deleteKey = basename($path);
 | 
						|
                    $collNode = $this->list->getNodeForPath($parentPath);
 | 
						|
                    if ($collNode instanceof ArrayNode) {
 | 
						|
                        $collNode->removeIndex($deleteKey);
 | 
						|
                        $this->list->parseTree();
 | 
						|
                        $this->redrawEditor();
 | 
						|
                    } elseif ($collNode instanceof ObjectNode) {
 | 
						|
                        $collNode->unset($deleteKey);
 | 
						|
                        $this->list->parseTree();
 | 
						|
                        $this->redrawEditor();
 | 
						|
                    } else {
 | 
						|
                        $this->term->setCursor(1, $h);
 | 
						|
                        echo "\e[97;41mCan only delete from object, array\e[K\e[0m";
 | 
						|
                    }
 | 
						|
                    break;
 | 
						|
 | 
						|
                // FIXME make sure this clones; editing a clone updates original as well
 | 
						|
                // case 'd':
 | 
						|
                //     $node = $this->list->getNodeForIndex($this->currentRow);
 | 
						|
                //     $coll = $this->list->findNearestCollection($this->currentRow, true);
 | 
						|
                //     if (!$coll)
 | 
						|
                //         break;
 | 
						|
                //     $collNode = $this->list->getNodeForIndex($coll);
 | 
						|
                //     if ($collNode instanceof ArrayNode) {
 | 
						|
                //         $collNode->append(clone $node);
 | 
						|
                //         $this->list->parseTree();
 | 
						|
                //         $this->redrawEditor();
 | 
						|
                //     } else {
 | 
						|
                //         $this->term->setCursor(1, $h);
 | 
						|
                //         echo "\e[97;41mCan only duplicate in arrays, node={$this->currentRow}, coll={$coll}\e[K\e[0m";
 | 
						|
                //     }
 | 
						|
                //     break;
 | 
						|
 | 
						|
                case 'i':
 | 
						|
                    $coll = $this->list->findNearestCollection($this->currentRow);
 | 
						|
                    $node = $this->list->getNodeForIndex($coll);
 | 
						|
                    if ($node instanceof ObjectNode) {
 | 
						|
                        $key = $this->ask("\e[97mkey:\e[0m ");
 | 
						|
                        if ($key === null) {
 | 
						|
                            $this->redrawInfoBar();
 | 
						|
                            break;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
 | 
						|
                    $value = $this->ask("\e[97mvalue:\e[0m ");
 | 
						|
                    if ($value !== null) {
 | 
						|
                        $value = json_decode($value);
 | 
						|
                        $valueNode = match (true) {
 | 
						|
                            is_array($value) => new ArrayNode([]),
 | 
						|
                            is_object($value) => new ObjectNode([]),
 | 
						|
                            default => new ValueNode($value)
 | 
						|
                        };
 | 
						|
                        if ($node instanceof ArrayNode) {
 | 
						|
                            $node->append($valueNode);
 | 
						|
                        } elseif ($node instanceof ObjectNode) {
 | 
						|
                            $node->set($key, $valueNode);
 | 
						|
                        }
 | 
						|
                        $this->list->parseTree();
 | 
						|
                        $this->redrawEditor();
 | 
						|
                    } else {
 | 
						|
                        $this->redrawInfoBar();
 | 
						|
                    }
 | 
						|
                    break;
 | 
						|
 | 
						|
                case 'Q':
 | 
						|
                case "\x03":
 | 
						|
                    $this->running = false;
 | 
						|
                    break;
 | 
						|
 | 
						|
                case null:
 | 
						|
                    break;
 | 
						|
                default:
 | 
						|
                    $this->term->setCursor(1, $h, true);
 | 
						|
                    echo $read; 
 | 
						|
                    sleep(1);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // for ($n = 0; $n < 20; $n++) {
 | 
						|
        //     $this->currentRow = $n;
 | 
						|
        //     $this->redrawEditor();
 | 
						|
        //     sleep(1);
 | 
						|
        // }
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
    public function ask(string $prompt, $value = ''): ?string
 | 
						|
    {
 | 
						|
        $plainPrompt = preg_replace('<\e\[.+?m>', '', $prompt);
 | 
						|
        $promptLen = mb_strlen($plainPrompt);
 | 
						|
        [$w,$h] = $this->term->getSize();
 | 
						|
 | 
						|
        $prompting = true;
 | 
						|
        $cursorPos = mb_strlen($value);
 | 
						|
        while ($prompting) {
 | 
						|
            $this->term->setCursor(1, $h);
 | 
						|
            echo $prompt."\e[0m\e[K".$value;
 | 
						|
            $this->term->setCursor($promptLen+$cursorPos+1, $h, true);
 | 
						|
            while (null === ($ch = $this->term->readKey())) {
 | 
						|
                usleep(10000);
 | 
						|
            }
 | 
						|
            if (mb_strlen($ch) == 1) {
 | 
						|
                if (ord($ch) < 32) {
 | 
						|
                    if (ord($ch) == 13) {
 | 
						|
                        $this->term->setCursor(1, $h);
 | 
						|
                        echo "\e[0m\e[K";
 | 
						|
                        return $value;
 | 
						|
                    }
 | 
						|
                    if (ord($ch) == 3) {
 | 
						|
                        $this->term->setCursor(1, $h);
 | 
						|
                        echo "\e[0m\e[K";
 | 
						|
                        return null;
 | 
						|
                    } 
 | 
						|
                } elseif (ord($ch) == 127) {
 | 
						|
                    if ($cursorPos > 0) {
 | 
						|
                        $value = substr($value, 0, $cursorPos - 1) . substr($value, $cursorPos);
 | 
						|
                        $cursorPos--;
 | 
						|
                    }
 | 
						|
                } else {
 | 
						|
                    $value = mb_substr($value, 0, $cursorPos) . $ch . mb_substr($value, $cursorPos);
 | 
						|
                    $cursorPos++;
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                switch ($ch) {
 | 
						|
                    case "k{LEFT}":
 | 
						|
                        if ($cursorPos > 0)
 | 
						|
                            $cursorPos--;
 | 
						|
                        break;
 | 
						|
                    case "k{RIGHT}":
 | 
						|
                        if ($cursorPos < mb_strlen($value))
 | 
						|
                            $cursorPos++;
 | 
						|
                        break;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
    public function redrawEditor()
 | 
						|
    {
 | 
						|
        [$w,$h] = $this->term->getSize();
 | 
						|
 | 
						|
        while ($this->currentRow < $this->scrollOffset) $this->scrollOffset--;
 | 
						|
        while ($this->currentRow > $h + $this->scrollOffset - 3) $this->scrollOffset++;
 | 
						|
 | 
						|
        $path = $this->list->getPathForIndex($this->currentRow);
 | 
						|
        $node = $this->list->getNodeForIndex($this->currentRow);
 | 
						|
        
 | 
						|
        $this->term->setCursor(1, $h-1);
 | 
						|
        echo "\e[44;37m\e[K\e[1m{$this->shortfilename}\e[22m#\e[3m{$path}\e[37;23m";
 | 
						|
 | 
						|
        //$this->term->setCursor(1, $h);
 | 
						|
        echo " = ";
 | 
						|
        if ($node instanceof ValueNode) {
 | 
						|
            echo json_encode($node->value, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
 | 
						|
        } elseif ($node instanceof ArrayNode) {
 | 
						|
            echo "[…]";
 | 
						|
        } elseif ($node instanceof ObjectNode) {
 | 
						|
            echo "{…}";
 | 
						|
        }
 | 
						|
        echo "\e[0m";
 | 
						|
 | 
						|
        $this->redrawInfoBar();
 | 
						|
 | 
						|
        // $this->term->setCursor(1, $h);
 | 
						|
        // echo "\e[0;37m\e[K";
 | 
						|
 | 
						|
        for ($n = 0; $n < $h - 2; $n++) {
 | 
						|
            $this->list->drawEntry($n + 1, $n + $this->scrollOffset, $w, ($n + $this->scrollOffset == $this->currentRow));
 | 
						|
        }
 | 
						|
        // $this->term->setCursor(1, 1);
 | 
						|
        // echo "\e[90;3m«Empty»\e[0m";
 | 
						|
    }
 | 
						|
 | 
						|
    private function redrawInfoBar()
 | 
						|
    {
 | 
						|
        [$w,$h] = $this->term->getSize();
 | 
						|
 | 
						|
        $keys = [
 | 
						|
            'e' => 'Edit',
 | 
						|
            'E' => 'Edit key',
 | 
						|
            'i' => 'Insert',
 | 
						|
            'D' => 'Delete',
 | 
						|
            'C' => 'Copy',
 | 
						|
            'P' => 'Paste',
 | 
						|
            '^C' => 'Exit',
 | 
						|
        ];
 | 
						|
 | 
						|
        $this->term->setCursor(1, $h);
 | 
						|
        echo "\e[0m\e[K";
 | 
						|
        foreach ($keys as $key=>$info)
 | 
						|
            echo " \e[1m{$key}\e[2m \e[3m{$info}\e[0m \e[90m\u{2502}\e[0m";
 | 
						|
    }
 | 
						|
 | 
						|
}
 |