859 lines
30 KiB
PHP
859 lines
30 KiB
PHP
<?php
|
|
|
|
namespace NoccyLabs\JsonEdit\Editor;
|
|
|
|
use NoccyLabs\JsonEdit\List\TreeList;
|
|
use NoccyLabs\JsonEdit\Settings;
|
|
use NoccyLabs\JsonEdit\Terminal\Terminal;
|
|
use NoccyLabs\JsonEdit\Tree\ArrayNode;
|
|
use NoccyLabs\JsonEdit\Tree\CollapsibleNode;
|
|
use NoccyLabs\JsonEdit\Tree\ObjectNode;
|
|
use NoccyLabs\JsonEdit\Tree\Tree;
|
|
use NoccyLabs\JsonEdit\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";
|
|
|
|
private bool $running = true;
|
|
|
|
private bool $modified = false;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param Terminal $term
|
|
*/
|
|
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);
|
|
|
|
$this->setWindowTitle($this->shortfilename." - JSONEdit");
|
|
}
|
|
|
|
/**
|
|
* Read a file into the Tree and TreeList
|
|
*
|
|
* @param string $filename
|
|
* @return void
|
|
*/
|
|
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->setWindowTitle($this->shortfilename." - JSONEdit");
|
|
$this->document->load($doc);
|
|
$this->list->parseTree();
|
|
}
|
|
|
|
/**
|
|
* Load a document from array/object
|
|
*
|
|
< * @param mixed $document
|
|
* @return void
|
|
*/
|
|
public function loadDocument(mixed $document): void
|
|
{
|
|
$this->document->load($document);
|
|
$this->list->parseTree();
|
|
}
|
|
|
|
/**
|
|
* Run the editor
|
|
*
|
|
* @return void
|
|
*/
|
|
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':
|
|
$this->doEditKey();
|
|
break;
|
|
|
|
case 'e':
|
|
$this->doEditValue();
|
|
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();
|
|
$this->modified = true;
|
|
} elseif ($collNode instanceof ObjectNode) {
|
|
$collNode->unset($deleteKey);
|
|
$this->list->parseTree();
|
|
$this->redrawEditor();
|
|
$this->modified = true;
|
|
} else {
|
|
$this->showMessage("\e[97;41mCan only delete from object, array");
|
|
}
|
|
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':
|
|
$menu = new Menu($this->term, [
|
|
'value' => "Insert Value",
|
|
'object' => "Insert Object{}",
|
|
'array' => "Insert Array[]",
|
|
//'sep0' => "---",
|
|
//'__paste' => "Paste from clipboard",
|
|
], 'Insert');
|
|
$this->redrawInfoBar([ '^C' => 'Cancel', '↑/↓' => 'Select option', 'Enter' => 'Accept' ]);
|
|
$sel = $menu->display(0, 0, 30, 0, "value");
|
|
$this->redrawEditor();
|
|
switch ($sel) {
|
|
case 'value':
|
|
$this->doInsertValue();
|
|
$this->modified = true;
|
|
break;
|
|
case 'array':
|
|
$this->doInsertValue('[]');
|
|
$this->modified = true;
|
|
break;
|
|
case 'object':
|
|
$this->doInsertValue('{}');
|
|
$this->modified = true;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'H':
|
|
$this->doShowAbout();
|
|
break;
|
|
|
|
case 'h':
|
|
$this->doShowHelp();
|
|
break;
|
|
|
|
case 'i':
|
|
$this->doInsertValue();
|
|
$this->modified = true;
|
|
break;
|
|
|
|
case "\x18": // ctrl-x
|
|
|
|
if ($this->modified) {
|
|
$menu = new Menu($this->term, [
|
|
'cancel' => "Return to editor",
|
|
'save' => "Save changes",
|
|
'discard' => "Discard changes",
|
|
//'sep0' => "---",
|
|
//'__paste' => "Paste from clipboard",
|
|
], 'Insert');
|
|
$this->redrawInfoBar([ '^C' => 'Cancel', '↑/↓' => 'Select option', 'Enter' => 'Accept' ]);
|
|
$sel = $menu->display(0, 0, 30, 0, "value");
|
|
$this->redrawEditor();
|
|
switch ($sel) {
|
|
case 'cancel':
|
|
break;
|
|
case 'save':
|
|
if ($this->doWriteFile()) {
|
|
$this->running = false;
|
|
}
|
|
break;
|
|
case 'discard':
|
|
$this->running = false;
|
|
break;
|
|
}
|
|
} else {
|
|
$this->running = false;
|
|
}
|
|
|
|
break;
|
|
|
|
case "g":
|
|
Settings::$indentationGuides = !Settings::$indentationGuides;
|
|
$this->redrawEditor();
|
|
$this->showMessage("Toggle indentation guides (g)");
|
|
break;
|
|
|
|
case "b":
|
|
Settings::$collapseBefore = !Settings::$collapseBefore;
|
|
$this->redrawEditor();
|
|
$this->showMessage("Toggle collapse icon before item (b)");
|
|
break;
|
|
|
|
case "t":
|
|
Settings::$tailLine = !Settings::$tailLine;
|
|
$this->redrawEditor();
|
|
$this->showMessage("Toggle tail line (t)");
|
|
break;
|
|
|
|
case "+":
|
|
$node = $this->list->getNodeForIndex($this->currentRow);
|
|
if ($node instanceof CollapsibleNode) {
|
|
$node->collapse(!$node->isCollapsed());
|
|
$this->list->parseTree();
|
|
$this->redrawEditor();
|
|
}
|
|
break;
|
|
|
|
case "-":
|
|
foreach ($this->list as $path => $entry) {
|
|
$node = $entry->node;
|
|
if ($path == "/") {
|
|
if ($node instanceof CollapsibleNode) {
|
|
$node->collapse(false);
|
|
}
|
|
} else {
|
|
if ($node instanceof CollapsibleNode) {
|
|
$node->collapse(true);
|
|
}
|
|
}
|
|
}
|
|
$this->currentRow = 0;
|
|
$this->list->parseTree();
|
|
$this->redrawEditor();
|
|
break;
|
|
|
|
case "k{LEFT}":
|
|
$node = $this->list->getNodeForIndex($this->currentRow);
|
|
if ($node instanceof CollapsibleNode) {
|
|
if ($node->isCollapsed()) {
|
|
$path = $this->list->getPathForIndex($this->currentRow);
|
|
$parent = $this->list->getIndexForPath(dirname($path));
|
|
if ($parent) {
|
|
$this->currentRow = $parent;
|
|
}
|
|
}
|
|
$node->collapse(true);
|
|
$this->list->parseTree();
|
|
$this->redrawEditor();
|
|
} else {
|
|
$path = $this->list->getPathForIndex($this->currentRow);
|
|
$parent = $this->list->getIndexForPath(dirname($path));
|
|
if ($parent) {
|
|
$this->currentRow = $parent;
|
|
}
|
|
$this->redrawEditor();
|
|
}
|
|
break;
|
|
|
|
case "k{RIGHT}":
|
|
$node = $this->list->getNodeForIndex($this->currentRow);
|
|
if ($node instanceof CollapsibleNode) {
|
|
$node->collapse(false);
|
|
$this->list->parseTree();
|
|
$this->redrawEditor();
|
|
}
|
|
break;
|
|
|
|
case "q":
|
|
Settings::$editorQuotedKeys = !Settings::$editorQuotedKeys;
|
|
$this->redrawEditor();
|
|
$this->showMessage("Toggle quoted keys (q)");
|
|
break;
|
|
|
|
case "c":
|
|
Settings::$compactGroups = !Settings::$compactGroups;
|
|
$this->list->parseTree();
|
|
$this->redrawEditor();
|
|
$this->showMessage("Toggle compact groups (c)");
|
|
break;
|
|
|
|
case "\x03": // ctrl-c
|
|
$this->showMessage("\e[30;43mPress Ctrl-X to exit.");
|
|
break;
|
|
|
|
case "\x0e": // ctrl-n
|
|
$this->document->load((object)[]);
|
|
$this->list->parseTree();
|
|
$this->filename = "untitled.json";
|
|
$this->shortfilename = "untitled.json";
|
|
$this->modified = false;
|
|
$this->redrawEditor();
|
|
break;
|
|
|
|
case "\x12": // ctrl-r
|
|
$this->doReadFile();
|
|
break;
|
|
|
|
case "\x17": // ctrl-w
|
|
$this->doWriteFile();
|
|
break;
|
|
|
|
case null:
|
|
break;
|
|
default:
|
|
$this->term->setCursor(1, $h, true);
|
|
echo sprintf("%s %02x %03d", ctype_print($read)?$read:'.', ord($read), ord($read));
|
|
sleep(1);
|
|
}
|
|
}
|
|
|
|
Settings::save(SETTINGS_FILE);
|
|
|
|
// for ($n = 0; $n < 20; $n++) {
|
|
// $this->currentRow = $n;
|
|
// $this->redrawEditor();
|
|
// sleep(1);
|
|
// }
|
|
|
|
}
|
|
|
|
/**
|
|
* Handler for read file command (ctrl-R)
|
|
*
|
|
* @return void
|
|
*/
|
|
private function doReadFile(): void
|
|
{
|
|
[$w,$h] = $this->term->getSize();
|
|
|
|
$readFrom = $this->ask("\e[33mRead from:\e[0m ", "");
|
|
|
|
if (!file_exists($readFrom)) {
|
|
$this->showMessage("\e[97;41mFile does not exist");
|
|
return;
|
|
}
|
|
if (!is_readable($readFrom)) {
|
|
$this->showMessage("\e[97;41mFile not readable");
|
|
return;
|
|
}
|
|
$ext = strtolower(pathinfo($readFrom, PATHINFO_EXTENSION));
|
|
switch ($ext) {
|
|
case 'json':
|
|
$doc = json_decode(file_get_contents($readFrom));
|
|
break;
|
|
case 'yml':
|
|
case 'yaml':
|
|
$doc = Yaml::parseFile($readFrom);
|
|
break;
|
|
default:
|
|
$this->showMessage("\e[97;41mUnable to read format: {$ext}");
|
|
return;
|
|
}
|
|
|
|
$this->filename = $readFrom;
|
|
$this->shortfilename = basename($readFrom);
|
|
|
|
$this->document->load($doc);
|
|
$this->currentRow = 0;
|
|
$this->list->parseTree();
|
|
$this->redrawEditor();
|
|
|
|
$this->setWindowTitle($this->shortfilename." - JSONEdit");
|
|
|
|
$this->showMessage("\e[97;42mLoaded {$readFrom}");
|
|
}
|
|
|
|
/**
|
|
* Handler for the write file command (ctrl-W)
|
|
*
|
|
* @return void
|
|
*/
|
|
private function doWriteFile(): bool
|
|
{
|
|
[$w,$h] = $this->term->getSize();
|
|
|
|
$saveTo = $this->ask("\e[33mWrite to:\e[0m ", $this->filename);
|
|
if (!$saveTo) {
|
|
return false;
|
|
}
|
|
|
|
$doc = $this->document->save();
|
|
|
|
|
|
$ext = strtolower(pathinfo($saveTo, PATHINFO_EXTENSION));
|
|
switch ($ext) {
|
|
case 'json':
|
|
file_put_contents($saveTo, json_encode($doc, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)."\n");
|
|
break;
|
|
case 'yml':
|
|
case 'yaml':
|
|
$doc = json_decode(json_encode($doc), true);
|
|
file_put_contents($saveTo, Yaml::dump($doc));
|
|
break;
|
|
default:
|
|
$this->showMessage("\e[97;41mUnable to write format: {$ext}");
|
|
return false;
|
|
}
|
|
|
|
$this->filename = $saveTo;
|
|
$this->shortfilename = basename($saveTo);
|
|
$this->modified = false;
|
|
$this->redrawEditor();
|
|
|
|
$this->setWindowTitle($this->shortfilename." - JSONEdit");
|
|
|
|
$this->showMessage("\e[97;42mWrote to {$saveTo}");
|
|
return true;
|
|
|
|
}
|
|
|
|
private function doShowHelp(): void
|
|
{
|
|
[$w,$h] = $this->term->getSize();
|
|
|
|
$text = <<<EOT
|
|
Welcome to JSONEdit! The editor you have missed all this time without even knowing it!
|
|
|
|
# QuickStart
|
|
|
|
To get started, press I (shift-i) and add something to the document. Use the arrow keys to get around. To cancel a prompt, close a menu or close a dialog, press ctrl-C. When you are happy with your work, press ctrl-W and enter a filename to write. You can also press ctrl-R and read in a new file, overwriting your masterpiece.
|
|
|
|
## Useful keys
|
|
|
|
↑↓ Navigate values in document
|
|
h Show this help
|
|
H Show information about the app and license
|
|
i Insert a new value
|
|
I Insert value, array or object
|
|
e Edit selected value
|
|
E Edit selected key
|
|
D Delete selected key
|
|
c Toggle compact list view
|
|
q Toggle quoted keys in list view
|
|
^W Write to file
|
|
^R Read from file
|
|
^N New document with empty object
|
|
^C Cancel/Exit
|
|
|
|
## Doing stuff
|
|
|
|
### Adding keys or values
|
|
To add a key or a value, navigate to a value in an array or object, or to a specific array or object, and press "i". You will be prompted for the value, and for objects the key.
|
|
You can also press "I" to add arrays and objects. Just select what you want to add in the menu and press enter.
|
|
|
|
### Editing keys
|
|
You can edit keys on objects. For this, press "E".
|
|
|
|
### Editing values
|
|
To edit a value, press "e". The value is verbatim JSON, so strings should be quoted and all that. Anything that is unparsable JSON will be used as is, resulting in a string.
|
|
|
|
### Loading and Saving files
|
|
To load a file, press ^R and enter the filename to read. To write to a file, press ^W and enter the filename to write to.
|
|
|
|
### YAML or JSON?
|
|
There is no need to select YAML or JSON mode. All operations work the same, and the format is determined on load or save.
|
|
|
|
# Disclaimer
|
|
|
|
This is beta software, if not alpha. It kinda works, but there will be issues. Feel free to help out with a patch, or by filing bug reports.
|
|
Known issues include:
|
|
|
|
* Comments are not preserved.
|
|
* Files are overwritten without confirmation.
|
|
* There is no command mode, no search.
|
|
* Some things just don't work yet.
|
|
* Unhandled keys will appear in the bottom left of the screen with a delay.
|
|
* There are crashes, and lock-ups. Data corruption is a possibility.
|
|
|
|
# Support
|
|
|
|
Go to https://dev.noccylabs.info/noccy/jsonedit to find the source code, issue tracker, and learn more about the project!
|
|
EOT;
|
|
|
|
$width = min(90, $w - 10);
|
|
$height = min(40, $h - 6);
|
|
$left = round(($w / 2) - ($width / 2));
|
|
$top = round(($h / 2) - ($height / 2));
|
|
|
|
$msg = new MessageBox($this->term, $text, "Help");
|
|
$this->redrawInfoBar([ '↑/↓' => 'Scroll', '^C' => 'Close' ]);
|
|
$msg->display($left, $top, $width, $height);
|
|
$this->redrawEditor();
|
|
|
|
}
|
|
|
|
private function doShowAbout(): void
|
|
{
|
|
[$w,$h] = $this->term->getSize();
|
|
|
|
$text = <<<EOT
|
|
## JSONEdit Alpha - Copyright (C) 2024, NoccyLabs
|
|
|
|
Licensed under GNU GPL v3.0 or later.
|
|
|
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
EOT;
|
|
|
|
$width = 60;
|
|
$height = 20;
|
|
$left = round(($w / 2) - ($width / 2));
|
|
$top = round(($h / 2) - ($height / 2));
|
|
|
|
$msg = new MessageBox($this->term, $text, "About JSONEdit");
|
|
$this->redrawInfoBar([ '↑/↓' => 'Scroll', '^C' => 'Close' ]);
|
|
$msg->display($left, $top, $width, $height);
|
|
$this->redrawEditor();
|
|
|
|
}
|
|
|
|
/**
|
|
* Edit selected key
|
|
*
|
|
* @return void
|
|
*/
|
|
private function doEditKey(): void
|
|
{
|
|
[$w,$h] = $this->term->getSize();
|
|
|
|
//$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[0;33mnew key:\e[0m ", $entry->key);
|
|
if (!empty($newVal)) {
|
|
$parent->node->rename($entry->key, $newVal);
|
|
$entry->key = $newVal;
|
|
$this->list->parseTree();
|
|
$this->redrawEditor();
|
|
}
|
|
} else {
|
|
// $this->term->setCursor(1, $h);
|
|
// echo "\e[97;41mCan only edit keys on objects\e[K\e[0m";
|
|
$this->showMessage("\e[97;41mCan only edit keys on objects");
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Edit selected value
|
|
*
|
|
* @return void
|
|
*/
|
|
private function doEditValue(): void
|
|
{
|
|
[$w,$h] = $this->term->getSize();
|
|
|
|
//$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);
|
|
// If the string decodes to null, but isn't 'null', treat it as a string
|
|
if ($val === null && $newVal !== 'null') {
|
|
$val = $newVal;
|
|
}
|
|
$node->value = $val;
|
|
$this->modified = true;
|
|
$this->redrawEditor();
|
|
} else {
|
|
$this->redrawInfoBar();
|
|
}
|
|
} else {
|
|
$this->showMessage("\e[97;41mCan not edit array/object");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert a new value
|
|
*
|
|
* @param mixed $value
|
|
* @return void
|
|
*/
|
|
private function doInsertValue(mixed $value = null): void
|
|
{
|
|
$coll = $this->list->findNearestCollection($this->currentRow);
|
|
$node = $this->list->getNodeForIndex($coll);
|
|
if ($node instanceof ObjectNode) {
|
|
$key = $this->ask("\e[0;33mkey:\e[0m ");
|
|
if (empty($key)) {
|
|
$this->redrawInfoBar();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ($value === null)
|
|
$value = $this->ask("\e[0;32mvalue:\e[0m ");
|
|
if ($value !== null) {
|
|
$newvalue = json_decode($value);
|
|
// If the string decodes to null, but isn't 'null', treat it as a string
|
|
if ($newvalue === null && $value !== 'null') {
|
|
$newvalue = $value;
|
|
}
|
|
$value = $newvalue;
|
|
|
|
$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->modified = true;
|
|
$this->list->parseTree();
|
|
$this->redrawEditor();
|
|
} else {
|
|
$this->redrawInfoBar();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ask for input
|
|
*
|
|
* @param string $prompt
|
|
* @param string $value
|
|
* @return string|null
|
|
*/
|
|
public function ask(string $prompt, $value = ''): ?string
|
|
{
|
|
$plainPrompt = preg_replace('<\e\[.+?m>', '', $prompt);
|
|
$promptLen = mb_strlen($plainPrompt);
|
|
[$w,$h] = $this->term->getSize();
|
|
|
|
$available = $w - $promptLen - 1;
|
|
|
|
$prompting = true;
|
|
$cursorPos = mb_strlen($value);
|
|
$scrollPos = max(0, $cursorPos - $available);
|
|
while ($prompting) {
|
|
while (($cursorPos - $scrollPos) > $available) {
|
|
$scrollPos++;
|
|
}
|
|
while (($cursorPos - $scrollPos) < 0) {
|
|
$scrollPos--;
|
|
}
|
|
$cursorOffs = $cursorPos - $scrollPos;
|
|
$this->term->setCursor(1, $h);
|
|
echo $prompt."\e[0m\e[K".mb_substr($value, $scrollPos, $available);
|
|
$this->term->setCursor($promptLen+$cursorOffs+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) {
|
|
// ctrl-c
|
|
$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;
|
|
case "k{HOME}":
|
|
$cursorPos = 0;
|
|
break;
|
|
case "k{END}":
|
|
$cursorPos = mb_strlen($value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Redraw the editor
|
|
*
|
|
* @return void
|
|
*/
|
|
public function redrawEditor()
|
|
{
|
|
[$w,$h] = $this->term->getSize();
|
|
|
|
// Jump to the tail line if the cursor is past the end of the entry list
|
|
if ($this->currentRow > count($this->list)) $this->currentRow = count($this->list);
|
|
|
|
// Make sure the selection is in view
|
|
while ($this->currentRow < $this->scrollOffset) $this->scrollOffset--;
|
|
while ($this->currentRow > $h + $this->scrollOffset - 3) $this->scrollOffset++;
|
|
|
|
// Nudge back so the tail line is visible but not selected
|
|
if ($this->currentRow == count($this->list)) $this->currentRow--;
|
|
|
|
$path = $this->list->getPathForIndex($this->currentRow);
|
|
$node = $this->list->getNodeForIndex($this->currentRow);
|
|
|
|
$modified = $this->modified ? "\e[31m*\e[37m" : "";
|
|
$this->term->setCursor(1, $h-1);
|
|
echo "\e[40;37m\e[K{$modified}\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";
|
|
}
|
|
|
|
/**
|
|
* Redraw the info bar
|
|
*
|
|
* @return void
|
|
*/
|
|
private function redrawInfoBar(?array $keys = null)
|
|
{
|
|
[$w,$h] = $this->term->getSize();
|
|
|
|
if (!$keys) {
|
|
$keys = [
|
|
'h' => 'Help',
|
|
'D' => 'Delete',
|
|
];
|
|
$node = $this->list->getNodeForIndex($this->currentRow);
|
|
if ($node instanceof ValueNode) {
|
|
$keys = [ ...$keys,
|
|
'e' => 'Edit',
|
|
];
|
|
}
|
|
if ($node instanceof ObjectNode || $node instanceof ArrayNode) {
|
|
$keys = [ ...$keys,
|
|
'I' => 'Insert…',
|
|
];
|
|
}
|
|
if ($this->modified) {
|
|
$keys = [ ...$keys,
|
|
'^N' => 'New',
|
|
'^W' => 'Write',
|
|
];
|
|
}
|
|
$keys = [ ...$keys,
|
|
'^R' => 'Read',
|
|
'^X' => 'Exit',
|
|
];
|
|
}
|
|
|
|
$this->term->setCursor(1, $h);
|
|
echo "\e[37;40m\e[K";
|
|
foreach ($keys as $key=>$info)
|
|
echo "\e[2m\u{f104}\e[22;97;1m{$key}\e[22;37;2m\u{f105} \e[22;36m{$info}\e[37m ";
|
|
//echo "\e[37;40;2m\u{e0b6}\e[7;37m{$key} \e[22m {$info} \e[27m\u{e0b4}\e[0m";
|
|
//echo " \e[1m{$key}\e[2m \e[3m{$info}\e[0m \e[90m\u{2502}\e[0m";
|
|
}
|
|
|
|
private function showMessage(string $message): void
|
|
{
|
|
[$w,$h] = $this->term->getSize();
|
|
$this->term->setCursor(1, $h);
|
|
echo "\e[0m".$message."\e[K\e[0m";
|
|
|
|
}
|
|
|
|
private function setWindowTitle(string $title): void
|
|
{
|
|
echo "\e]2;{$title}\x07";
|
|
}
|
|
|
|
}
|