diff --git a/README.md b/README.md index 7924745..1a9cd02 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ This is an editor for JSON files, that also supports YAML. +> [!WARNING] +> This software is still very much beta. That means there will be bugs. Please +> report them, and don't go doing stupid stuff like editing your live configuration +> files in place. + ## Features - Interactive terminal TUI application @@ -35,7 +40,8 @@ $ mv bin/jsonedit.phar ~/bin/jsonedit ## Using JSONEdit is controlled using hotkeys. The essential keys are always displayed -at the bottom of the screen. +at the bottom of the screen. Press **h** for the help, which lists all available +keys. ### Creating a document from scratch diff --git a/src/Editor/Editor.php b/src/Editor/Editor.php index bf1ac3c..7441554 100644 --- a/src/Editor/Editor.php +++ b/src/Editor/Editor.php @@ -10,11 +10,14 @@ use NoccyLabs\JsonEdit\Tree\CollapsibleNode; use NoccyLabs\JsonEdit\Tree\ObjectNode; use NoccyLabs\JsonEdit\Tree\Tree; use NoccyLabs\JsonEdit\Tree\ValueNode; +use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; class Editor { + public static string $helpText; + private Tree $document; private TreeList $list; @@ -201,7 +204,6 @@ class Editor break; case "\x18": // ctrl-x - if ($this->modified) { $menu = new Menu($this->term, [ 'cancel' => "Return to editor", @@ -321,12 +323,28 @@ class Editor $this->showMessage("Toggle compact groups (c)"); break; + case "\x0d": // enter + $node = $this->list->getNodeForIndex($this->currentRow); + if ($node instanceof ValueNode) { + if ($node->value === true) { + $node->value = false; + $this->modified = true; + $this->redrawEditor(); + } elseif ($node->value === false) { + $node->value = true; + $this->modified = true; + $this->redrawEditor(); + } else { + $this->doEditValue(); + } + } + break; + case "\x03": // ctrl-c $this->showMessage("\e[30;43mPress Ctrl-X to exit."); break; case "\x0e": // ctrl-n - if ($this->modified) { $menu = new Menu($this->term, [ 'cancel' => "Return to editor", @@ -349,6 +367,8 @@ class Editor case 'discard': break; } + $this->modified = false; + } $this->document->load((object)[]); @@ -359,6 +379,10 @@ class Editor $this->redrawEditor(); break; + case "\x0F": // ctrl-o + $this->doOpenFile(); + break; + case "\x12": // ctrl-r $this->doReadFile(); break; @@ -369,6 +393,7 @@ class Editor case null: break; + default: $this->term->setCursor(1, $h, true); echo sprintf("%s %02x %03d", ctype_print($read)?$read:'.', ord($read), ord($read)); @@ -476,75 +501,68 @@ class Editor } + private function doOpenFile() + { + $wd = getcwd(); + while (true) { + $items = [ + dirname($wd) => sprintf( + "%-30s %10s %20s", + "Parent Directory", + "", + "-" + ) + ]; + $files = glob($wd."/*"); + foreach ($files as $file) { + $items[$file] = sprintf( + "%-30s %10s %20s", + is_dir($file) ? ("<".basename($file).">") : (basename($file)), + is_dir($file) ? "" : filesize($file), + date("Y-m-d H:i:s", filemtime($file)) + ); + } + $menu = new Menu($this->term, $items, $wd); + $sel = $menu->display(0, 0, 70, 20, null); + if ($sel === null) { + $this->redrawEditor(); + return; + } + if (is_dir($sel)) { + $wd = $sel; + } + if (is_file($sel)) { + $body = file_get_contents($sel); + $json = json_decode($body); + if ($json !== null) { + $this->loadDocument($json); + break; + } + try { + $yaml = Yaml::parse($body); + $this->loadDocument($yaml); + break; + } catch (ParseException $e) { + $this->showMessage("\e[41;93mUnsupported file format"); + } + } + } + $this->filename = $sel; + $this->shortfilename = basename($sel); + $this->redrawEditor(); + $this->showMessage("\e[42;97mOpened {$this->shortfilename}"); + } + private function doShowHelp(): void { [$w,$h] = $this->term->getSize(); - $text = <<term, $text, "Help"); + $msg = new MessageBox($this->term, self::$helpText, "Help"); $this->redrawInfoBar([ '↑/↓' => 'Scroll', '^C' => 'Close' ]); $msg->display($left, $top, $width, $height); $this->redrawEditor(); @@ -622,7 +640,14 @@ class Editor if ($node instanceof ValueNode) { $val = json_encode($node->value, JSON_UNESCAPED_SLASHES); $newVal = $this->ask("\e[33;1mnew value:\e[0m ", $val); + // We get null on ctrl-C, so non-null is input if ($newVal !== null) { + // Break out if the value has not changed + if ($newVal === $val) { + $this->redrawInfoBar(); + return; + } + // Attempt to decode the value $val = json_decode($newVal); // If the string decodes to null, but isn't 'null', treat it as a string if ($val === null && $newVal !== 'null') { @@ -815,7 +840,9 @@ class Editor $path = $this->list->getPathForIndex($this->currentRow); $node = $this->list->getNodeForIndex($this->currentRow); - + + ob_start(); + $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"; @@ -841,6 +868,8 @@ class Editor } // $this->term->setCursor(1, 1); // echo "\e[90;3m«Empty»\e[0m"; + + ob_end_flush(); } /** @@ -902,3 +931,63 @@ class Editor } } + +Editor::$helpText = << "\e[33m", 'integer' => "\e[94m", 'double' => "\e[96m", - 'boolean' => "\e[35m", + 'boolean' => ($value?"\e[92m":"\e[32m"), 'NULL' => "\e[31m", default => "", };