From 1602b873bc9840edcecb2adaea1851e5eda0a381 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Wed, 23 Jun 2021 17:21:03 -0600 Subject: [PATCH] WIP CLI --- .../src/nat/ArchiveController.hx | 15 ++- .../src/nat/ArchiveController.kiss | 106 ++++++++++++++++++ .../nat-archive-tool/src/nat/ArchiveUI.hx | 15 ++- projects/nat-archive-tool/src/nat/CLI.kiss | 40 +++++-- 4 files changed, 156 insertions(+), 20 deletions(-) diff --git a/projects/nat-archive-tool/src/nat/ArchiveController.hx b/projects/nat-archive-tool/src/nat/ArchiveController.hx index 9499f56c..d3ca7dc1 100644 --- a/projects/nat-archive-tool/src/nat/ArchiveController.hx +++ b/projects/nat-archive-tool/src/nat/ArchiveController.hx @@ -3,21 +3,26 @@ package nat; import kiss.Prelude; import kiss.List; import kiss.Operand; - import haxe.Constraints; -enum CommandArgument { +enum CommandArgType { SelectedEntry; SelectedEntries(min:Null, max:Null); - Text(minLength:Null, maxLength:Null); + Text(minLength:Null, maxLength:Null); // max length is a float so Math.POSITIVE_INFINITY can be used Number(min:Null, max:Null, inStepsOf:Null); - Entry; + OneEntry; // This constructor must be disambiguated from the typedef "Entry" Entries(min:Null, max:Null); } +typedef CommandArg = { + name:String, + type:CommandArgType +}; + typedef Command = { - args:Array, + args:Array, handler:Function + // Command handlers need to return a ChangeSet }; typedef ChangeSet = Array; diff --git a/projects/nat-archive-tool/src/nat/ArchiveController.kiss b/projects/nat-archive-tool/src/nat/ArchiveController.kiss index 2fdac40c..edfec812 100644 --- a/projects/nat-archive-tool/src/nat/ArchiveController.kiss +++ b/projects/nat-archive-tool/src/nat/ArchiveController.kiss @@ -4,9 +4,115 @@ (defmethod selectEntries [:Array e] (set selectedEntries e)) +(defmethod selectLastChangeSet [] + (set selectedEntries lastChangeSet)) + +(defmethod :Void collectAndValidateArg [:CommandArg arg :Dynamic->Void continuation] + (case arg.type + (SelectedEntry + (if (= 1 selectedEntries.length) + (continuation (first selectedEntries)) + (ui.reportError "The requested command expects 1 entry to be selected. You have selected: $selectedEntries.length"))) + ((SelectedEntries min max) + (unless min (set min 0)) + // TODO might want to optimize this O(n) count operation by pre-calculating it + (unless max (set max (count archive.entries))) + (if !(<= min selectedEntries.length max) + (ui.reportError "The requested command expects between $min and $max entries to be selected. You have selected: $selectedEntries.length") + (continuation selectedEntries))) + ((Text minLength maxLength) + (unless minLength (set minLength 0)) + (unless maxLength (set maxLength Math.POSITIVE_INFINITY)) + (ui.enterText + "${arg.name} (${minLength}-${maxLength} characters):" + (lambda :Void [text] + (if !(<= minLength text.length maxLength) + (ui.reportError "The requested command expected a string between $minLength and $maxLength characters long. You entered: $text.length characters") + (continuation text))) + minLength + maxLength)) + ((Number min max inStepsOf) + (unless min (set min Math.NEGATIVE_INFINITY)) + (unless max (set max Math.POSITIVE_INFINITY)) + (let [&mut prompt "${arg.name} (${min}-${max}"] + (when inStepsOf + (+= prompt " in steps of ${inStepsOf}")) + (+= prompt "):") + + (ui.enterNumber + prompt + (lambda :Void [number] + (let [minMaxError + "The requested command expected a number between $min and $max" + stepError + "$minMaxError in steps of $inStepsOf" + youEntered + ". You entered: $number"] + (if (or + !(<= min number max) + (and inStepsOf !(= 0 (% (- number min) inStepsOf)))) + (if inStepsOf + (ui.reportError "${stepError}$youEntered") + (ui.reportError "${minMaxError}$youEntered")) + (continuation number)))) + min + max + inStepsOf))) + (OneEntry + (ui.chooseEntry + "${arg.name}:" + archive + continuation)) + ((Entries min max) + (unless min (set min 0)) + // TODO might want to optimize this O(n) count operation by pre-calculating it + (unless max (set max (count archive.entries))) + (ui.chooseEntries + "${arg.name}:" + archive + (lambda :Void [:Array entries] + (if (or + (> min entries.length) + (< max entries.length)) + + (ui.reportError "The requested command expects between $min and $max entries. You chose: $entries.length") + (continuation selectedEntries))))))) + +/*(defmethod :Void runCommand [command] + (let [collectedArgs + [] + callHandler + (lambda [] + (set lastChangeSet (the ChangeSet (Reflect.callMethod command.handler collectedArgs))) + (ui.handleChanges lastChangeSet))]) + // If the command has no arguments, it can run synchronously with no hassle + (if !command.args + (callHandler) + // To facilitate asynchronous arg input via UI, we need to construct an insanely complicated nested callback to give the UI + (let [argsToCollect + (reversed command.args) + composeArgCollector + (lambda :Void->Void [:CommandArg arg :Void->Void lastResolver] + (lambda :Void [] + (case arg.type + ((SelectedEntry + + (ui.chooseEntry arg.prompt ))) + (collectedArgs.push arg) + (lastResolver) + )) + resolve (lambda [:Dynamic arg] + ()) + ]) + (doFor arg (reversed command.args) + ) + ) + )*/ + (defnew [&prop :Archive archive &prop :ArchiveUI ui] [&mut :Array selectedEntries [] + &mut :ChangeSet lastChangeSet [] :Map commands (new Map)] ) \ No newline at end of file diff --git a/projects/nat-archive-tool/src/nat/ArchiveUI.hx b/projects/nat-archive-tool/src/nat/ArchiveUI.hx index 1515584c..960196a2 100644 --- a/projects/nat-archive-tool/src/nat/ArchiveUI.hx +++ b/projects/nat-archive-tool/src/nat/ArchiveUI.hx @@ -6,25 +6,30 @@ interface ArchiveUI { /** * Prompt the user to enter text */ - function enterText(?minLength:Int, ?maxLength:Int):String; + function enterText(prompt:String, resolve:(String) -> Void, ?minLength:Int, ?maxLength:Float):Void; /** * Prompt the user to enter a number */ - function enterNumber(?min:Float, ?max:Float, ?inStepsOf:Float):Float; + function enterNumber(prompt:String, resolve:(Float) -> Void, ?min:Float, ?max:Float, ?inStepsOf:Float):Void; /** * Prompt the user to choose a single Entry */ - function chooseEntry(archive:Archive):Entry; + function chooseEntry(prompt:String, archive:Archive, resolve:(Entry) -> Void):Void; /** * Prompt the user to choose multiple Entries */ - function chooseEntries(archive:Archive, ?min:Int, ?max:Int):Array; + function chooseEntries(prompt:String, archive:Archive, resolve:(Array) -> Void, ?min:Int, ?max:Float):Void; /** * Update the interface to reflect changes made to Entries through commands */ function handleChanges(changeSet:ChangeSet):Void; -} \ No newline at end of file + + /** + * Tell the user that something is wrong + */ + function reportError(error:String):Void; +} diff --git a/projects/nat-archive-tool/src/nat/CLI.kiss b/projects/nat-archive-tool/src/nat/CLI.kiss index 0977e965..cd6c1e0e 100644 --- a/projects/nat-archive-tool/src/nat/CLI.kiss +++ b/projects/nat-archive-tool/src/nat/CLI.kiss @@ -4,18 +4,38 @@ (new ArchiveController (new Archive archiveDir) (new CLI))] - )) + (controller.collectAndValidateArg (object name "numerical" type (Number -5 5 1)) ->:Void val ~val) + (controller.collectAndValidateArg (object name "string" type (Text 5 null)) ->:Void val ~val))) -(defmethod enterText [&opt minLength maxLength] - "") +(defnew []) -(defmethod enterNumber [&opt min max inStepsOf] - 0) +(defmethod :Void enterText [prompt resolve &opt minLength maxLength] + (Sys.print "$prompt ") + (loop + (let [entered (.toString (.readLine (Sys.stdin)))] + (if !(<= minLength entered.length maxLength) + (Sys.print "Try again? ") + {(resolve entered) + (break)})))) -(defmethod chooseEntry [archive] - null) +(defmethod :Void enterNumber [prompt resolve &opt min max inStepsOf] + (Sys.print "$prompt ") + (loop + (let [entered (Std.parseFloat (.toString (.readLine (Sys.stdin))))] + (if + (or + !(<= min entered max) + (and inStepsOf !(= 0 (% (- entered min) inStepsOf)))) + (Sys.print "Try again? ") + {(resolve entered) + (break)})))) -(defmethod chooseEntries [archive &opt min max] - []) +(defmethod :Void chooseEntry [prompt archive resolve] + (resolve null)) -(defmethod :Void handleChanges [changeSet] 0) \ No newline at end of file +(defmethod :Void chooseEntries [prompt archive resolve &opt min max] + (resolve [])) + +(defmethod handleChanges [changeSet]) + +(defmethod :Void reportError [error] ~error) \ No newline at end of file