258 lines
12 KiB
Plaintext
258 lines
12 KiB
Plaintext
(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<Entry> 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<Dynamic> 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<nat.Entry> ,name)
|
|
((Text _) `:String ,name)
|
|
((VarText _) `:Array<String> ,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<String> commandNames [])
|
|
|
|
(method isSelected [:Entry e]
|
|
!(= -1 (_selectedEntries.indexOf e)))
|
|
|
|
(method getSelectedEntries []
|
|
(_selectedEntries.copy))
|
|
|
|
(defNew [&prop :Archive archive
|
|
&prop :ArchiveUI ui]
|
|
[&mut :Array<Entry> _selectedEntries []
|
|
&mut :ChangeSet lastChangeSet []
|
|
:Map<String,Command> 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))
|
|
(archive.addSystem (new KeyShortcutSystem this))
|
|
(archive.addSystem (new DLSystem))
|
|
|
|
// 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
|
|
(CreateMediaEntry medium 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 AddKeyShortcut [e SelectedEntry description (Text null)]
|
|
(addComponent archive e KeyShortcut description))
|
|
|
|
(defCommand AddNATCommand [e (SelectedEntries null null) command (Text null)]
|
|
(doFor e e (addComponent archive e NATCommand command)))
|
|
|
|
(defCommand AddNATCommands [e (SelectedEntries null null) commands (VarText null)]
|
|
(doFor e e (addComponent archive e NATCommands commands)))
|
|
|
|
(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<Entry> 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))
|