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"; } }