(load "Lib.kiss") (method :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 maxLength) (unless maxLength (set maxLength Math.POSITIVE_INFINITY)) (ui.enterText "${arg.name} (up to ${maxLength} characters):" (lambda :Void [text] (if !(<= text.length maxLength) (ui.reportError "The requested command expected a string up to $maxLength characters long. You entered: $text.length characters") (continuation text))) maxLength)) ((VarText maxLength) (unless maxLength (set maxLength Math.POSITIVE_INFINITY)) (let [collectedText [] &mut :Void->Void enterTextAgain null _enterTextAgain ->:Void (ui.enterText "${arg.name} (up to ${maxLength} characters):" (lambda :Void [text] (if !text (continuation collectedText) (if !(<= text.length maxLength) (ui.reportError "The requested command expected a list of strings up to $maxLength characters long. You entered: $text.length characters") {(collectedText.push text) (enterTextAgain)}))) maxLength)] (set enterTextAgain _enterTextAgain) (enterTextAgain))) ((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 1)) // 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 entries))) min max)) (null))) (method :Void->Void _composeArgCollector [:Array collectedArgs :CommandArg arg :Void->Void lastCollector] (lambda :Void [] (_collectAndValidateArg arg ->:Void [:Dynamic argValue] {(collectedArgs.push argValue) (lastCollector)}))) (method :Void tryRunCommand [:String commandName] (let [lowerCommandName (commandName.toLowerCase)] (if (commands.exists lowerCommandName) (_runCommand (dictGet commands lowerCommandName)) (ui.reportError "$commandName is not a valid command")))) (method :Void _runCommand [:Command command] (let [collectedArgs [] &mut lastCollector (lambda [] (set lastChangeSet (the ChangeSet (Reflect.callMethod null command.handler collectedArgs))) (when lastChangeSet (ui.handleChanges archive lastChangeSet)))] // To facilitate asynchronous arg input via UI, we need to construct an insanely complicated nested callback to give the UI (doFor arg (reverse command.args) (set lastCollector (_composeArgCollector collectedArgs arg lastCollector))) (lastCollector))) // TODO SelectedEntry and SelectedEntries functions should be stateful and use the actual // selected entries automatically (defMacro defCommand [name args &body body] (let [argPairs (groups (expList args) 2) methodArgs (for [name type] argPairs (exprCase type ((exprOr SelectedEntry OneEntry) `:nat.Entry ,name) ((exprOr (SelectedEntries _ _) (Entries _ _)) `:Array ,name) ((Text _) `:String ,name) ((VarText _) `:Array ,name) ((Number _ _ _) `:Float ,name))) commandArgs (for [name type] argPairs `(object name ,(symbolName name) type ,type))] `{ (method ,name [,@methodArgs] ,@body) // Preserve the capitalization of the command name for pretty help message (commandNames.push ,(symbolName name)) // Store the command name without capitalization for forgiving call conventions (dictSet commands ,(ReaderExp.StrExp (.toLowerCase (symbolNameValue name))) (object args [,@commandArgs] handler (the Function ,name)))})) (var :Array commandNames []) (method isSelected [:Entry e] !(= -1 (_selectedEntries.indexOf e))) (method getSelectedEntries [] (_selectedEntries.copy)) (defNew [&prop :Archive archive &prop :ArchiveUI ui] [&mut :Array _selectedEntries [] &mut :ChangeSet lastChangeSet [] :Map commands (new Map) :NameSystem nameSystem (new NameSystem)] (set ui.controller this) // Add systems! (archive.addSystem nameSystem) (archive.addSystem (new RemarkableAPISystem)) (archive.addSystem (new WikipediaImageSystem)) (archive.addSystem (new ImageAttachmentSystem)) // Just for testing: // (archive.addSystem (new AttachmentSystem ["jpg" "jpeg" "png"] ->[archive e files] ~files)) (archive.processSystems) (defCommand Help [] (ui.displayMessage (+ "Available commands:\n" (commandNames.join "\n"))) []) (load "SelectionCommands.kiss") (defCommand PrintSelectedEntries [entries (SelectedEntries null null)] (doFor e entries (ui.displayMessage (archive.fullString e))) []) (defCommand PrintComponent [entries (SelectedEntries null null) componentType (Text null)] (doFor e entries (if (e.components.exists componentType) (ui.displayMessage (dictGet e.components componentType)) (ui.displayMessage "Entry ${e.id} has no $componentType component"))) []) (defCommand CreateEntry [name (Text null)] [(archive.createEntry ->e (addComponent archive e Name name))]) (defCommand CreateEntries [names (VarText null)] // createEntry returns a list, so these lists must be flattened (flatten (for name names (CreateEntry name)))) (defCommand CreateMediaEntry [medium (Text null) name (Text null)] [(archive.createEntry ->e { (addComponent archive e Name name) (addTags archive e ["media" medium])})]) (defCommand CreateMediaEntries [medium (Text null) names (VarText null)] // createEntry returns a list, so these lists must be flattened (flatten (for name names (CreateEntry name)))) // TODO use Tag and VarTag arg types for AddTags and RemoveTags (defCommand AddTags [entries (SelectedEntries 1 null) tagsToAdd (VarText null)] (doFor e entries (addTags archive e tagsToAdd)) entries) // TODO this includes entries that already had the tag in the changeset (defCommand RemoveTags [entries (SelectedEntries 1 null) tagsToRemove (VarText null)] (doFor e entries (removeTags archive e tagsToRemove)) entries) // TODO this includes entries that didn't have the tag in the changeset (defCommand AddFiles [entries (SelectedEntries 1 null) // TODO add File and Files as an argument type for commands, ArchiveUI // TODO make tkinter file browser externs and use tkinter as the file picking mechanism for CLI files (VarText null)] (doFor e entries (addFiles archive e files)) entries) (method adjustImagePins [:Array entries increment] (doFor e entries (if (hasComponent e Images) (withWritableComponents archive e [images Images] (set images.pinnedImageIndex (max 0 (min (- images.imageFiles.length 1) (+ increment images.pinnedImageIndex))))) (ui.reportError "Entry $e has no Images component"))) entries) (defCommand PinNextImage [entries (SelectedEntries 1 null)] (adjustImagePins entries 1)) (defCommand PinPreviousImage [entries (SelectedEntries 1 null)] (adjustImagePins entries -1)) (defCommand SetScale [entries (SelectedEntries 1 null) scale (Number 0 null null)] (doFor e entries (if (hasComponent e Scale) (withWritableComponents archive e [scaleComponent Scale] (set scaleComponent scale)) (addComponent archive e Scale scale))) entries))