Souk21 / TW-commandpalette

A command palette for TiddlyWiki. Demo: https://souk21.github.io/TW-commandpalette/
Other
39 stars 5 forks source link

Suggested improvement with sample code - multiple search types #13

Open bepuzzled opened 3 years ago

bepuzzled commented 3 years ago

Hello, I absolutely love the TiddlyWiki command palette. The customizable search steps are invaluable and very cleverly programmed. I wanted to take it to the next level and allow different search types invoked with different keyboard shortcuts or prefixes. This lets the user customize the search filters used, based on the shortcut used (openPalette parameter).

This modified $:/core/modules/widgets/commandpalettewidget.js tiddler achieves this goal, for your consideration; sample search step dictionary included in the comment.

happy to submit a pull request if desired.


/*\
title: $:/core/modules/widgets/commandpalettewidget.js
type: application/javascript
module-type: widget

Command Palette Widget modified and annotated to support multiple search patterns
first problem is to support different keystrokes to enable different search modes (using parameters)
second problem is to extract the correct search settings with a modified format
e.g.
{
    "default": [
          {"filter": "[!is[system]!tag[todo]search:title[]]", "hint": "in title", "caret": "35"},
          {"filter": "[!is[system]!tag[todo]search:text[]]", "hint": "all", "caret": "34"}
    ],
    "/": [
          {"filter": "[!is[system]tag[todo]search:title[]]", "hint": "in title", "caret": "34"},
          {"filter": "[!is[system]tag[todo]search:text[]]", "hint": "all", "caret": "33"}
    ],
    "~": [
          {"filter": "[!is[system]tag[monster]search:title[]]", "hint": "in title", "caret": "37"},
          {"filter": "[!is[system]tag[monster]search:text[]]", "hint": "all", "caret": "36"}
    ]
}

\*/
(function () {

    /*jslint node: true, browser: true */
    /*global $tw: false */
    'use strict';

    var Widget = require('$:/core/modules/widgets/widget.js').widget;

    class CommandPaletteWidget extends Widget {
        constructor(parseTreeNode, options) {
            super(parseTreeNode, options);
            this.initialise(parseTreeNode, options);
            this.currentSelection = 0; //0 is nothing selected, 1 is first result,...
            this.symbolProviders = {};
            this.actions = [];
            this.triggers = [];
            this.searchType = 'default'; //
            this.blockProviderChange = false;
            this.defaultSettings = {
                maxResults: 15,
                maxResultHintSize: 45,
                neverBasic: false,
                showHistoryOnOpen: true,
                escapeGoesBack: true,
                alwaysPassSelection: true,
                theme: '$:/plugins/souk21/commandpalette/Compact.css',
            };
            this.settings = {};
            this.commandHistoryPath = '$:/plugins/souk21/commandpalette/CommandPaletteHistory';
            this.settingsPath = '$:/plugins/souk21/commandpalette/CommandPaletteSettings';
            this.searchStepsPath = '$:/plugins/souk21/commandpalette/CommandPaletteSearchSteps';
            this.customCommandsTag = '$:/tags/CommandPaletteCommand';
            this.themesTag = '$:/tags/CommandPaletteTheme';
            this.typeField = 'command-palette-type';
            this.nameField = 'command-palette-name';
            this.hintField = 'command-palette-hint';
            this.modeField = 'command-palette-mode';
            this.userInputField = 'command-palette-user-input';
            this.caretField = 'command-palette-caret';
            this.immediateField = 'command-palette-immediate';
            this.triggerField = 'command-palette-trigger';
        }

        actionStringBuilder(text) {
            return (e) => this.invokeActionString(text, this, e);
        }

        actionStringInput(action, hint, e) {
            this.blockProviderChange = true;
            this.allowInputFieldSelection = true;
            this.hint.innerText = hint;
            this.input.value = '';
            this.currentProvider = () => { };
            this.currentResolver = (e) => {
                this.invokeActionString(action, this, e, { 'commandpaletteinput': this.input.value });
                this.closePalette();
            }
            this.showResults([]);
            this.onInput(this.input.value);
        }

        invokeFieldMangler(tiddler, message, param, e) {
            let action = `<$fieldmangler tiddler="${tiddler}">
            <$action-sendmessage $message="${message}" $param="${param}"/>
            </$fieldmangler>`;
            this.invokeActionString(action, this, e);
        }

        //filter = (tiddler, terms) => [tiddlers]
        tagOperation(e, hintTiddler, hintTag, filter, allowNoSelection, message) {
            this.blockProviderChange = true;
            if (allowNoSelection) this.allowInputFieldSelection = true;
            this.currentProvider = this.historyProviderBuilder(hintTiddler);
            this.currentResolver = (e) => {
                if (this.currentSelection === 0) return;
                let tiddler = this.currentResults[this.currentSelection - 1].result.name;
                this.currentProvider = (terms) => {
                    this.currentSelection = 0;
                    this.hint.innerText = hintTag;
                    let searches = filter(tiddler, terms);
                    searches = searches.map(s => { return { name: s }; });
                    this.showResults(searches);
                }
                this.input.value = "";
                this.onInput(this.input.value);
                this.currentResolver = (e) => {
                    if (!allowNoSelection && this.currentSelection === 0) return;
                    let tag = this.input.value;
                    if (this.currentSelection !== 0) {
                        tag = this.currentResults[this.currentSelection - 1].result.name;
                    }
                    this.invokeFieldMangler(tiddler, message, tag, e);
                    if (!e.getModifierState('Shift')) {
                        this.closePalette();
                    } else {
                        this.onInput(this.input.value);
                    }
                }
            }
            this.input.value = "";
            this.onInput(this.input.value);
        }

        refreshThemes(e) {
            this.themes = this.getTiddlersWithTag(this.themesTag);
            let found = false;
            for (let theme of this.themes) {
                let themeName = theme.fields.title;
                if (themeName === this.settings.theme) {
                    found = true;
                    this.addTagIfNecessary(themeName, '$:/tags/Stylesheet', e);
                } else {
                    this.invokeFieldMangler(themeName, 'tm-remove-tag', '$:/tags/Stylesheet', e);
                }
            }
            if (found) return;
            this.addTagIfNecessary(this.defaultSettings.theme, '$:/tags/Stylesheet', e);
        }

        //Re-adding an existing tag changes modification date
        addTagIfNecessary(tiddler, tag, e) {
            if (this.hasTag(tiddler, tag)) return;
            this.invokeFieldMangler(tiddler, 'tm-add-tag', tag, e);
        }

        hasTag(tiddler, tag) {
            return $tw.wiki.getTiddler(tiddler).fields.tags.includes(tag);
        }

        refreshCommands() {
            this.actions = [];
            this.actions.push({ name: "Refresh Command Palette", action: (e) => { this.refreshCommandPalette(); this.promptCommand('') }, keepPalette: true });
            this.actions.push({ name: "Explorer", action: (e) => this.explorer(e), keepPalette: true });
            this.actions.push({ name: "See History", action: (e) => this.showHistory(e), keepPalette: true });
            this.actions.push({ name: "New Command Wizard", action: (e) => this.newCommandWizard(e), keepPalette: true });
            this.actions.push({
                name: "Add tag to tiddler",
                action: (e) => this.tagOperation(e, 'Pick tiddler to tag', 'Pick tag to add (⇧⏎ to add multiple)',
                    (tiddler, terms) => $tw.wiki.filterTiddlers(`[!is[system]tags[]] [is[system]tags[]] -[[${tiddler}]tags[]] +[search[${terms}]]`),
                    true,
                    'tm-add-tag'),
                keepPalette: true
            });
            this.actions.push({
                name: "Remove tag",
                action: (e) => this.tagOperation(e, 'Pick tiddler to untag', 'Pick tag to remove (⇧⏎ to remove multiple)',
                    (tiddler, terms) => $tw.wiki.filterTiddlers(`[[${tiddler}]tags[]] +[search[${terms}]]`),
                    false,
                    'tm-remove-tag'),
                keepPalette: true
            });

            //load up custom commands defined in tiddlers
            let commandTiddlers = this.getTiddlersWithTag(this.customCommandsTag);
            for (let tiddler of commandTiddlers) {
                if (!tiddler.fields[this.typeField] === undefined) continue;
                let name = tiddler.fields[this.nameField];
                let type = tiddler.fields[this.typeField];
                let text = tiddler.fields.text;
                if (text === undefined) text = '';
                let textFirstLine = text.match(/^.*/)[0];
                let hint = tiddler.fields[this.hintField];
                if (hint === undefined) hint = tiddler.fields[this.nameField];
                if (hint === undefined) hint = '';
                if (type === 'shortcut') {
                    let trigger = tiddler.fields[this.triggerField];
                    if (trigger === undefined) continue;
                    this.triggers.push({ name, trigger, text, hint });
                    continue;
                }
                if (!tiddler.fields[this.nameField] === undefined) continue;
                if (type === 'prompt') {
                    let immediate = !!tiddler.fields[this.immediateField];
                    let caret = tiddler.fields[this.caretField];
                    let action = { name: name, action: () => this.promptCommand(textFirstLine, caret), keepPalette: !immediate, immediate: immediate };
                    this.actions.push(action);
                    continue;
                }
                if (type === 'prompt-basic') {
                    let caret = tiddler.fields[this.caretField];
                    let action = { name: name, action: () => this.promptCommandBasic(textFirstLine, caret, hint), keepPalette: true };
                    this.actions.push(action);
                    continue;
                }
                if (type === 'message') {
                    this.actions.push({ name: name, action: (e) => this.tmMessageBuilder(textFirstLine)(e) });
                    continue;
                }
                if (type === 'actionString') {
                    let userInput = tiddler.fields[this.userInputField] !== undefined && tiddler.fields[this.userInputField] === 'true';
                    if (userInput) {
                        this.actions.push({ name: name, action: (e) => this.actionStringInput(text, hint, e), keepPalette: true });
                    } else {
                        this.actions.push({ name: name, action: (e) => this.actionStringBuilder(text)(e) });
                    }
                    continue;
                }
                if (type === 'history') {
                    let mode = tiddler.fields[this.modeField];
                    this.actions.push({ name: name, action: (e) => this.commandWithHistoryPicker(textFirstLine, hint, mode).handler(e), keepPalette: true });
                    continue;
                }
            }
        }

        newCommandWizard() {
            this.blockProviderChange = true;
            this.input.value = '';
            this.hint.innerText = 'Command Name';
            let name = '';
            let type = '';
            let hint = '';

            let messageStep = () => {
                this.input.value = '';
                this.hint.innerText = 'Enter Message';
                this.currentResolver = (e) => {
                    this.tmMessageBuilder('tm-new-tiddler',
                        {
                            title: '$:/' + name,
                            tags: this.customCommandsTag,
                            [this.typeField]: type,
                            [this.nameField]: name,
                            [this.hintField]: hint,
                            text: this.input.value
                        })(e);
                    this.closePalette();
                }
            }

            let hintStep = () => {
                this.input.value = '';
                this.hint.innerText = 'Enter hint';
                this.currentResolver = (e) => {
                    hint = this.input.value;
                    messageStep();
                }
            }

            let typeStep = () => {
                this.input.value = '';
                this.hint.innerText = 'Enter type (prompt, prompt-basic, message, actionString, history)'
                this.currentResolver = (e) => {
                    type = this.input.value;
                    if (type === 'history') {
                        hintStep();
                    } else {
                        this.tmMessageBuilder('tm-new-tiddler',
                            {
                                title: '$:/' + name,
                                tags: this.customCommandsTag,
                                [this.typeField]: type,
                                [this.nameField]: name
                            })(e);
                        this.closePalette();
                    }
                }
            }

            this.currentProvider = (terms) => { }
            this.currentResolver = (e) => {
                if (this.input.value.length === 0) return;
                name = this.input.value;
                typeStep();
            }
            this.showResults([]);
        }

        explorer(e) {
            this.blockProviderChange = true;
            this.input.value = '$:/';
            this.lastExplorerInput = '$:/';
            this.hint.innerText = 'Explorer (⇧⏎ to add multiple)';
            this.currentProvider = (terms) => this.explorerProvider('$:/', terms);
            this.currentResolver = (e) => {
                if (this.currentSelection === 0) return;
                this.currentResults[this.currentSelection - 1].result.action(e);
            }
            this.onInput();
        }

        explorerProvider(url, terms) {
            let switchFolder = (url) => {
                this.input.value = url;
                this.lastExplorerInput = this.input.value;
                this.currentProvider = (terms) => this.explorerProvider(url, terms);
                this.onInput();
            };
            if (!this.input.value.startsWith(url)) {
                this.input.value = this.lastExplorerInput;
            }
            this.lastExplorerInput = this.input.value;
            this.currentSelection = 0;
            let search = this.input.value.substr(url.length);
            let tiddlers = $tw.wiki.filterTiddlers(`[removeprefix[${url}]splitbefore[/]sort[]search[${search}]]`);
            let folders = [];
            let files = [];
            for (let tiddler of tiddlers) {
                if (tiddler.endsWith('/')) {
                    folders.push({ name: tiddler, action: (e) => switchFolder(`${url}${tiddler}`) });
                } else {
                    files.push({
                        name: tiddler, action: (e) => {
                            this.navigateTo(`${url}${tiddler}`);
                            if (!e.getModifierState('Shift')) {
                                this.closePalette();
                            }
                        }
                    });
                }
            }
            let topResult;
            if (url !== '$:/') {
                let splits = url.split('/');
                splits.splice(splits.length - 2);
                let parent = splits.join('/') + '/';
                topResult = { name: '..', action: (e) => switchFolder(parent) };
                this.showResults([topResult, ...folders, ...files]);
                return;
            }
            this.showResults([...folders, ...files]);
        }

        setSetting(name, value) {
            //doing the validation here too (it's also done in refreshSettings, so you can load you own settings with a json file)
            if (typeof value === 'string') {
                if (value === 'true') value = true;
                if (value === 'false') value = false;
            }
            this.settings[name] = value;
            $tw.wiki.setTiddlerData(this.settingsPath, this.settings);
        }

        //loadSettings?
        refreshSettings() {
            //get user or default settings
            this.settings = $tw.wiki.getTiddlerData(this.settingsPath, { ...this.defaultSettings });
            //Adding eventual missing properties to current user settings
            for (let prop in this.defaultSettings) {
                if (!this.defaultSettings.hasOwnProperty(prop)) continue;
                if (this.settings[prop] === undefined) {
                    this.settings[prop] = this.defaultSettings[prop];
                }
            }
            //cast all booleans
            for (let prop in this.settings) {
                if (!this.settings.hasOwnProperty(prop)) continue;
                if (typeof this.settings[prop] !== 'string') continue;
                if (this.settings[prop].toLowerCase() === 'true') this.settings[prop] = true;
                if (this.settings[prop].toLowerCase() === 'false') this.settings[prop] = false;
            }
        }

        //helper function to retrieve all tiddlers (+ their fields) with a tag
        getTiddlersWithTag(tag) {
            let tiddlers = $tw.wiki.getTiddlersWithTag(tag);
            return tiddlers.map(t => $tw.wiki.getTiddler(t));
        }

        //akin to a constructor, only is invoked upon widget creation
        render(parent, nextSibling) {
            this.parentDomNode = parent;
            this.execute();
            this.history = $tw.wiki.getTiddlerData(this.commandHistoryPath, { history: [] }).history;

            $tw.rootWidget.addEventListener('open-command-palette', (e) => this.openPalette(e));
            $tw.rootWidget.addEventListener('open-command-palette-selection', (e) => this.openPaletteSelection(e));
            $tw.rootWidget.addEventListener('insert-command-palette-result', (e) => this.insertSelectedResult(e));

            let inputAndMainHintWrapper = this.createElement('div', { className: 'inputhintwrapper' });
            this.div = this.createElement('div', { className: 'commandpalette' }, { display: 'none' });
            this.input = this.createElement('input', { type: 'text' });
            this.hint = this.createElement('div', { className: 'commandpalettehint commandpalettehintmain' });
            inputAndMainHintWrapper.append(this.input, this.hint);
            this.scrollDiv = this.createElement('div', { className: 'cp-scroll' });
            this.div.append(inputAndMainHintWrapper, this.scrollDiv);
            this.input.addEventListener('keydown', (e) => this.onKeyDown(e));
            this.input.addEventListener('input', () => this.onInput(this.input.value));
            window.addEventListener('click', (e) => this.onClick(e));
            parent.insertBefore(this.div, nextSibling);

            //loads the settings and filter steps
            this.refreshCommandPalette();

            //symbolProviders is a defined here as a dictionary of dictionaries (why here, and not in the constructor?)
            // the key is a character prefix and the value is a dictionary itself of two key value pairs for keys: "searcher" and "resolver"; 
            // The values are functions!! mapping an argument (or two) to a function call
            //the parseCommand(text) returns two functions (searcher, resolver) based on a dictionary lookup in symbolProviers, and a terms text string
            //searcher and resolver functions are assigned by key lookup in this dictionary if find() on the _array_ created from the dictionary keys is not undefined
            // the defaultProvider and defaultResolver are returned when find() is undefined
            this.symbolProviders['>'] = { searcher: (terms) => this.actionProvider(terms), resolver: (e) => this.actionResolver(e) };
            this.symbolProviders['#'] = { searcher: (terms) => this.tagListProvider(terms), resolver: (e) => this.tagListResolver(e) };
            this.symbolProviders['@'] = { searcher: (terms) => this.tagProvider(terms), resolver: (e) => this.defaultResolver(e) };
            this.symbolProviders['?'] = { searcher: (terms) => this.helpProvider(terms), resolver: (e) => this.helpResolver(e) };
            this.symbolProviders['['] = { searcher: (terms, hint) => this.filterProvider(terms, hint), resolver: (e) => this.filterResolver(e) };
            this.symbolProviders['+'] = { searcher: (terms) => this.createTiddlerProvider(terms), resolver: (e) => this.createTiddlerResolver() };
            this.symbolProviders['|'] = { searcher: (terms) => this.settingsProvider(terms), resolver: (e) => this.settingsResolver() };
            this.currentResults = [];
            this.currentProvider = {};
        }

        //loads json data structure that includes array of search steps, that is 
        //filter expressions with a caret position at which search terms are entered
        //this function loads the array of filter expressions as an array of functions provided by searchStepBuilder
        refreshSearchSteps() {
            this.searchSteps = [];

            let steps = $tw.wiki.getTiddlerData(this.searchStepsPath);
            //steps = steps[this.searchtype];
            //searchtype is set to "steps" by default, based on the format of 
            // $:/plugins/souk21/commandpalette/CommandPaletteSearchSteps in the original plugin
            //The Object.entries() method returns an array of a given object's own enumerable string-keyed property [key, value] pairs, 

            //for (const [key, value] of Object.entries(steps)) {
            //console.log("key="+key);
            //console.log("steps="+steps);
            //steps = steps.steps;
            //this pushes the correct searchSteps to the stack, each being a function that is generated by searchStepBuilder
            //that places the search term within the filter at the caret position
            for (var type in steps) {
                //console.log("+++++loading type "+type);
                for (let step of steps[type]) {
                    //console.log("-----> with step "+step.hint);
                    this.searchSteps.push(this.searchStepBuilder(step.filter, step.caret, step.hint, type)); //store the type associated with each step (like it is done for hints)
                }
            }
        }

        refreshCommandPalette() {
            this.refreshSettings();
            this.refreshThemes();
            this.refreshCommands();
            this.refreshSearchSteps();
        }

        updateCommandHistory(command) {
            this.history = Array.from(new Set([command.name, ...this.history]));
            $tw.wiki.setTiddlerData(this.commandHistoryPath, { history: this.history });
        }

        historyProviderBuilder(hint, mode) {
            return (terms) => {
                this.currentSelection = 0;
                this.hint.innerText = hint;
                let results;
                if (mode !== undefined && mode === 'drafts') {
                    results = $tw.wiki.filterTiddlers('[has:field[draft.of]]');
                } else if (mode !== undefined && mode === 'story') {
                    results = $tw.wiki.filterTiddlers('[list[$:/StoryList]]');
                } else {
                    results = this.getHistory();
                }
                results = results.map(r => { return { name: r } });
                this.showResults(results);
            };
        }

        commandWithHistoryPicker(message, hint, mode) {
            let handler = (e) => {
                this.blockProviderChange = true;
                this.allowInputFieldSelection = true;
                this.currentProvider = provider;
                this.currentResolver = resolver;
                this.input.value = '';
                this.onInput(this.input.value);
            }
            let provider = this.historyProviderBuilder(hint, mode);
            let resolver = (e) => {
                if (this.currentSelection === 0) return;
                let title = this.currentResults[this.currentSelection - 1].result.name;
                this.parentWidget.dispatchEvent({
                    type: message,
                    param: title,
                    tiddlerTitle: title,
                });
                this.closePalette();
            }
            return {
                handler,
                provider,
                resolver
            }
        }

        //is triggered when the palette first opens, and then for every key strokes
        onInput(text) { //reparses the command on every key strokes

            //prevent provider changes -> this is set in a bunch of places; maybe whenever we've started adding to the initial & already parsed text?
            if (this.blockProviderChange) { 
                this.currentProvider(text);
                this.setSelectionToFirst();
                return;
            }
            //repoints the resolvers and providers based on the input text
            let { resolver, provider, terms } = this.parseCommand(text);
            this.currentResolver = resolver;
            this.currentProvider = provider;
            this.currentProvider(terms);
            this.setSelectionToFirst();
        }

        parseCommand(text) {
            let terms = "";
            //prefix is the first character of the input, which can either be typed,
            //or passed in using the parameter of the openPalette call (see openPalette(e) {)
            let prefix = text.substr(0, 1);  
            let resolver;
            let provider;

            //this looks through the triggers table to see if the command text starts with any of the trigger characters
            //triggers seem to be only when the users define custom commands with triggers
            let shortcut = this.triggers.find(t => text.startsWith(t.trigger));

            //there is a trigger match ! yay!
            if (shortcut !== undefined) {
                resolver = (e) => {
                    let inputWithoutShortcut = this.input.value.substr(shortcut.trigger.length);
                    this.invokeActionString(shortcut.text, this, e, { 'commandpaletteinput': inputWithoutShortcut });
                    this.closePalette();
                }
                //this is a function that is (probably) activate with a reduce call?
                provider = (terms) => {
                    //adjusting the hint variable (for display) based on information in the triggers table
                    this.hint.innerText = shortcut.hint;
                    this.showResults([]);
                }

            //no trigger match
            } else {

                //tries to match the prefix in the array of dictionary keys
                //prefix is the first character of the input, which can either be typed,
                //or passed in using the parameter of the openPalette call (see openPalette(e) {)
                let providerSymbol = Object.keys(this.symbolProviders).find(p => p === prefix);

                //no match
                if (providerSymbol === undefined) {

                    //check to see if the prefix matches a key from the searchSteps tiddler's dictionary, and set the this.searchType accordingly
                    let steps = $tw.wiki.getTiddlerData(this.searchStepsPath);
                    var result = steps[prefix];
                    console.log("trying to find the prefix in the search steps perhaps: "+result);

                    //we have a match, enable search steps filtering by setting this.searchType
                    if (result !== undefined) {
                        this.searchType = prefix;
                        terms = text.substring(1); //ignores the prefix (stays displayed, but isn't used for searching)
                    } else {
                        this.searchType = "default"; //default search steps
                        terms = text;
                    }

                    resolver = this.defaultResolver; //search resolver
                    provider = this.defaultProvider; //search provider

                }
                else {
                    //two dictionary key lookup when (array).find() above is defined
                    provider = this.symbolProviders[providerSymbol].searcher; //this is a function
                    resolver = this.symbolProviders[providerSymbol].resolver; //this is a function
                    terms = text.substring(1); //ignores the prefix (stays displayed, but isn't used for searching)
                }
            }
            return { resolver, provider, terms }
        }

        onClick(e) {
            if (this.isOpened && !this.div.contains(e.target)) {
                this.closePalette();
            }
        }
        openPaletteSelection(e) {
            let selection = this.getCurrentSelection();
            e.param = selection;
            this.openPalette(e);
        }
        openPalette(e) {
            this.isOpened = true;
            this.allowInputFieldSelection = false;
            this.goBack = undefined;
            this.blockProviderChange = false;
            let activeElement = this.getActiveElement();
            this.previouslyFocused = { element: activeElement, start: activeElement.selectionStart, end: activeElement.selectionEnd, caretPos: activeElement.selectionEnd };
            this.input.value = '';

            //was there a parameter provided when calling the openPalette function
            //for example, mapping a different keyboad shortcut to trigger a different behaviour
            //$tw.rootWidget.invokeActionString('<$action-sendmessage $message="open-command-palette" $param="Z"/>',$tw.rootWidget);
            if (e.param !== undefined) {
                this.input.value = e.param;
            }
            //this appends the selected text (at the time the palette is open) to the input value
            if (this.settings.alwaysPassSelection) {
                this.input.value += this.getCurrentSelection();
            }
            this.currentSelection = 0; //?

            //behave as if someone had typed what is in input.value)
            this.onInput(this.input.value); //Trigger results on open 
            this.div.style.display = 'flex'; //display the palette bar?
            this.input.focus();
        }

        insertSelectedResult() {
            if (!this.isOpened) return;
            if (this.currentSelection === 0) return; //TODO: what to do here?
            let previous = this.previouslyFocused;
            let previousValue = previous.element.value;
            if (previousValue === undefined) return;
            let selection = this.currentResults[this.currentSelection - 1].result.name;
            if (previous.start !== previous.end) {
                this.previouslyFocused.element.value = previousValue.substring(0, previous.start) + selection + previousValue.substring(previous.end);
            } else {
                this.previouslyFocused.element.value = previousValue.substring(0, previous.start) + selection + previousValue.substring(previous.start);
            }
            this.previouslyFocused.caretPos = previous.start + selection.length;
            this.closePalette();
        }

        closePalette() {
            this.div.style.display = 'none';
            this.isOpened = false;
            this.focusAtCaretPosition(this.previouslyFocused.element, this.previouslyFocused.caretPos);
        }
        onKeyDown(e) {
            if (e.key === 'Escape') {
                //                                  \/ There's no previous state
                if (!this.settings.escapeGoesBack || this.goBack === undefined) {
                    this.closePalette();
                } else {
                    this.goBack();
                    this.goBack = undefined;
                }
            }
            else if (e.key === 'ArrowUp') {
                e.preventDefault();
                e.stopPropagation();
                let sel = this.currentSelection - 1;

                if (sel === 0) {
                    if (!this.allowInputFieldSelection) {
                        sel = this.currentResults.length;
                    }
                } else if (sel < 0) {
                    sel = this.currentResults.length;
                }
                this.setSelection(sel);
            }
            else if (e.key === 'ArrowDown') {
                e.preventDefault();
                e.stopPropagation();
                let sel = (this.currentSelection + 1) % (this.currentResults.length + 1);
                if (!this.allowInputFieldSelection && sel === 0 && this.currentResults.length !== 0) {
                    sel = 1;
                }
                this.setSelection(sel);
            }
            else if (e.key === 'Enter') {
                e.preventDefault();
                e.stopPropagation();
                this.validateSelection(e);
            }
        }
        addResult(result, id) {
            let resultDiv = this.createElement('div', { className: 'commandpaletteresult', innerText: result.name });
            if (result.hint !== undefined) {
                let hint = this.createElement('div', { className: 'commandpalettehint', innerText: result.hint });
                resultDiv.append(hint);
            }
            resultDiv.result = result;
            this.currentResults.push(resultDiv);
            resultDiv.addEventListener('click', (e) => { this.setSelection(id + 1); this.validateSelection(e); });
            this.scrollDiv.append(resultDiv);
        }
        validateSelection(e) {
            this.currentResolver(e);
        }

        //a resolver seems to be the handler when a selection is made/action is triggered;
        //the default resolver navigates to the tiddler shown in the provider's result list
        //or creates the tiddler matching the terms (text) if the shift key is pressed when the resolver is triggered (enter key or click?)
        defaultResolver(e) {
            if (e.getModifierState('Shift')) {
                this.input.value = '+' + this.input.value; //this resolver expects that the input starts with +
                this.createTiddlerResolver(e);
                return;
            }
            if (this.currentSelection === 0) return;
            let selectionTitle = this.currentResults[this.currentSelection - 1].result.name;
            this.closePalette();
            this.navigateTo(selectionTitle);
        }
        navigateTo(title) {
            this.parentWidget.dispatchEvent({
                type: 'tm-navigate',
                param: title,
                navigateTo: title
            });
        }

        showHistory() {
            this.hint.innerText = 'History';
            this.currentProvider = (terms) => {
                let results;
                if (terms.length === 0) {
                    results = this.getHistory();
                } else {
                    results = this.getHistory().filter(h => h.includes(terms));
                }
                results = results.map(r => { return { name: r, action: () => { this.navigateTo(r); this.closePalette(); } } });
                this.showResults(results);
            };
            this.currentResolver = (e) => {
                if (this.currentSelection === 0) return;
                this.currentResults[this.currentSelection - 1].result.action(e);
            };
            this.input.value = '';
            this.blockProviderChange = true;
            this.onInput(this.input.value);
        }

        setSelectionToFirst() {
            let sel = 1;
            if (this.allowInputFieldSelection || this.currentResults.length === 0) {
                sel = 0;
            }
            this.setSelection(sel)
        }

        setSelection(id) {
            this.currentSelection = id;
            for (let i = 0; i < this.currentResults.length; i++) {
                let selected = this.currentSelection === i + 1;
                this.currentResults[i].className = selected ? 'commandpaletteresult commandpaletteresultselected' : 'commandpaletteresult';
            }
            if (this.currentSelection === 0) {
                this.scrollDiv.scrollTop = 0;
                return;
            }
            let scrollHeight = this.scrollDiv.offsetHeight;
            let scrollPos = this.scrollDiv.scrollTop;
            let selectionPos = this.currentResults[this.currentSelection - 1].offsetTop;
            let selectionHeight = this.currentResults[this.currentSelection - 1].offsetHeight;

            if (selectionPos < scrollPos || selectionPos >= scrollPos + scrollHeight) {
                //select the closest scrolling position showing the selection
                let a = selectionPos;
                let b = selectionPos - scrollHeight + selectionHeight;
                a = Math.abs(a - scrollPos);
                b = Math.abs(b - scrollPos);
                if (a < b) {
                    this.scrollDiv.scrollTop = selectionPos;
                } else {
                    this.scrollDiv.scrollTop = selectionPos - scrollHeight + selectionHeight;
                }
            }
        }

        getHistory() {
            let history = $tw.wiki.getTiddlerData('$:/HistoryList');
            if (history === undefined) {
                history = [];
            }
            history = [...history.reverse().map(x => x.title), ...$tw.wiki.filterTiddlers('[list[$:/StoryList]]')];
            return Array.from(new Set(history.filter(t => this.tiddlerOrShadowExists(t))));
        }

        tiddlerOrShadowExists(title) {
            return $tw.wiki.tiddlerExists(title) || $tw.wiki.isShadowTiddler(title);
        }

        //a provider is simply a lister of search results
        //default provider is a search provider; 
        defaultProvider(terms) {
            this.hint.innerText = 'Search tiddlers (⇧⏎ to create)';
            let searches;
            if (terms.startsWith('\\')) terms = terms.substr(1); //user was escaping first character to avoid triggering a special functionality, so we ignore it
            if (terms.length === 0) {
                if (this.settings.showHistoryOnOpen) {
                    searches = this.getHistory().map(s => { return { name: s, hint: 'history' } });
                } else {
                    searches = [];
                }
            }
            else {
                //this magic calls upon the searchSteps functions by providing the terms !?!
                //array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

                // is this the spread operator?
                //the initial set is empty []
                //then the a variable designates the accumulator?
                //to which is appended the next result set (computed by the function that c represents, with the terms argument
                searches = this.searchSteps.reduce((a, c) => [...a, ...c(terms)], []).filter((step) => step.type === this.searchType); //magic? turns searchSteps into results

                searches = Array.from(new Set(searches));
            }
            this.showResults(searches);
        }

        //returns an instance of a filterTiddler function with the user supplied filter expression, dependent on a terms parameter
        //neither the searchSterpBuilder nor the function returned by searchStepBuilder return "search results" at this stage, not until 
        //the map reduce is invoked supplying the search term parameters
        searchStepBuilder(filter, caret, hint, type) {
            return (terms) => {
                //substitutes search term into filter expression for that search step
                let search = filter.substr(0, caret) + terms + filter.substr(caret);
                //takes a filter expression and turn it into a TW filterTiddler function call using map
                //map prototype
                let results = $tw.wiki.filterTiddlers(search).map(s => { return { name: s, hint: hint, type: type } }); //retun the type 
                return results;
            }
        }

        tagListProvider(terms) {
            this.currentSelection = 0;
            this.hint.innerText = 'Search tags';
            let searches;
            if (terms.length === 0) {
                searches = $tw.wiki.filterTiddlers('[!is[system]tags[]][is[system]tags[]][all[shadows]tags[]]');
            }
            else {
                searches = $tw.wiki.filterTiddlers('[all[]tags[]!is[system]search[' + terms + ']][all[]tags[]is[system]search[' + terms + ']][all[shadows]tags[]search[' + terms + ']]');
            }
            searches = searches.map(s => { return { name: s }; });
            this.showResults(searches);
        }
        tagListResolver(e) {
            if (this.currentSelection === 0) {
                let input = this.input.value.substr(1);
                let exist = $tw.wiki.filterTiddlers('[tag[' + input + ']]');
                if (!exist)
                    return;
                this.input.value = '@' + input;
                return;
            }
            let result = this.currentResults[this.currentSelection - 1];
            this.input.value = '@' + result.innerText;
            this.onInput(this.input.value);
        }
        tagProvider(terms) {
            this.currentSelection = 0;
            this.hint.innerText = 'Search tiddlers with @tag(s)';
            let searches = [];
            if (terms.length !== 0) {
                let { tags, searchTerms, tagsFilter } = this.parseTags(this.input.value);
                let taggedTiddlers = $tw.wiki.filterTiddlers(tagsFilter);

                if (taggedTiddlers.length !== 0) {
                    if (tags.length === 1) {
                        let tag = tags[0];
                        let tagTiddlerExists = this.tiddlerOrShadowExists(tag);
                        if (tagTiddlerExists && searchTerms.some(s => tag.includes(s))) searches.push(tag);
                    }
                    searches = [...searches, ...taggedTiddlers];
                }
            }
            searches = searches.map(s => { return { name: s } });
            this.showResults(searches);
        }

        parseTags(input) {
            let splits = input.split(' ').filter(s => s !== '');
            let tags = [];
            let searchTerms = [];
            for (let i = 0; i < splits.length; i++) {
                if (splits[i].startsWith('@')) {
                    tags.push(splits[i].substr(1));
                    continue;
                }
                searchTerms.push(splits[i]);
            }
            let tagsFilter = `[all[tiddlers+system+shadows]${tags.reduce((a, c) => { return a + 'tag[' + c + ']' }, '')}]`;
            if (searchTerms.length !== 0) {
                tagsFilter = tagsFilter.substr(0, tagsFilter.length - 1); //remove last ']'
                tagsFilter += `search[${searchTerms.join(' ')}]]`;
            }
            return { tags, searchTerms, tagsFilter };
        }

        settingsProvider(terms) {
            this.currentSelection = 0;
            this.hint.innerText = 'Select the setting you want to change';
            let isNumerical = (terms) => terms.length !== 0 && terms.match(/\D/gm) === null;
            let isBoolean = (terms) => terms.length !== 0 && terms.match(/(true\b)|(false\b)/gmi) !== null;
            this.showResults([
                { name: 'Theme (currently ' + this.settings.theme.match(/[^\/]*$/) + ')', action: () => this.promptForThemeSetting() },
                this.settingResultBuilder('Max results', 'maxResults', 'Choose the maximum number of results', isNumerical, 'Error: value must be a positive integer'),
                this.settingResultBuilder('Show history on open', 'showHistoryOnOpen', 'Chose whether to show the history when you open the palette', isBoolean, 'Error: value must be \'true\' or \'false\''),
                this.settingResultBuilder('Escape to go back', 'escapeGoesBack', 'Chose whether ESC should go back when possible', isBoolean, 'Error: value must be \'true\' or \'false\''),
                this.settingResultBuilder('Use selection as search query', 'alwaysPassSelection', 'Chose your current selection is passed to the command palette', isBoolean, 'Error: value must be \'true\' or \'false\''),
                this.settingResultBuilder('Never Basic', 'neverBasic', 'Chose whether to override basic prompts to show filter operation', isBoolean, 'Error: value must be \'true\' or \'false\''),
                this.settingResultBuilder('Field preview max size', 'maxResultHintSize', 'Choose the maximum hint length for field preview', isNumerical, 'Error: value must be a positive integer'),
            ]);
        }

        settingResultBuilder(name, settingName, hint, validator, errorMsg) {
            return { name: name + ' (currently ' + this.settings[settingName] + ')', action: () => this.promptForSetting(settingName, hint, validator, errorMsg) }
        }

        settingsResolver(e) {
            if (this.currentSelection === 0) return;
            this.goBack = () => {
                this.input.value = '|';
                this.blockProviderChange = false;
                this.onInput(this.input.value);
            }
            this.currentResults[this.currentSelection - 1].result.action();
        }

        promptForThemeSetting() {
            this.blockProviderChange = true;
            this.allowInputFieldSelection = false;
            this.currentProvider = (terms) => {
                this.currentSelection = 0;
                this.hint.innerText = 'Choose a theme';
                let defaultValue = this.defaultSettings['theme'];
                let results = [{ name: 'Revert to default value: ' + defaultValue.match(/[^\/]*$/), action: () => { this.setSetting('theme', defaultValue); this.refreshThemes(); } }];
                for (let theme of this.themes) {
                    let name = theme.fields.title;
                    let shortName = name.match(/[^\/]*$/);
                    let action = () => { this.setSetting('theme', name); this.refreshThemes(); }
                    results.push({ name: shortName, action: action });
                }
                this.showResults(results);
            }
            this.currentResolver = (e) => {
                this.currentResults[this.currentSelection - 1].result.action(e);
            }
            this.input.value = '';
            this.onInput(this.input.value);
        }

        //Validator = (terms) => bool
        promptForSetting(settingName, hint, validator, errorMsg) {
            this.blockProviderChange = true;
            this.allowInputFieldSelection = true;
            this.currentProvider = (terms) => {
                this.currentSelection = 0;
                this.hint.innerText = hint;
                let defaultValue = this.defaultSettings[settingName];
                let results = [{ name: 'Revert to default value: ' + defaultValue, action: () => this.setSetting(settingName, defaultValue) }];
                if (!validator(terms)) {
                    results.push({ name: errorMsg });
                }
                this.showResults(results);
            };
            this.currentResolver = (e) => {
                if (this.currentSelection === 0) {
                    let input = this.input.value;
                    if (validator(input)) {
                        this.setSetting(settingName, input);
                        this.goBack = undefined;
                        this.blockProviderChange = false;
                        this.allowInputFieldSelection = false;
                        this.promptCommand('|');
                    }
                } else {
                    let action = this.currentResults[this.currentSelection - 1].result.action;
                    if (action) {
                        action();
                        this.goBack = undefined;
                        this.blockProviderChange = false;
                        this.allowInputFieldSelection = false;
                        this.promptCommand('|');
                    }
                }
            }
            this.input.value = this.settings[settingName];
            this.onInput(this.input.value);
        }

        showResults(results) {
            for (let cur of this.currentResults) {
                cur.remove();
            }
            this.currentResults = [];
            let resultCount = 0;
            for (let result of results) {
                this.addResult(result, resultCount);
                resultCount++;
                if (resultCount >= this.settings.maxResults)
                    break;
            }
        }

        tmMessageBuilder(message, params = {}) {
            return (e) => {
                let event = {
                    type: message,
                    paramObject: params,
                    event: e,
                };
                this.parentWidget.dispatchEvent(event);
            };
        }
        actionProvider(terms) {
            this.currentSelection = 0;
            this.hint.innerText = 'Search commands';
            let results;
            if (terms.length === 0) {
                results = this.getCommandHistory();
            }
            else {
                results = this.actions.filter(a => a.name.toLowerCase().includes(terms.toLowerCase()));
            }
            this.showResults(results);
        }

        helpProvider(terms) { //TODO: tiddlerify?
            this.currentSelection = 0;
            this.hint.innerText = 'Help';
            let searches = [
                { name: '... Search', action: () => this.promptCommand('') },
                { name: '> Commands', action: () => this.promptCommand('>') },
                { name: '+ Create tiddler with title', action: () => this.promptCommand('+') },
                { name: '# Search tags', action: () => this.promptCommand('#') },
                { name: '@ List tiddlers with tag', action: () => this.promptCommand('@') },
                { name: '[ Filter operation', action: () => this.promptCommand('[') },
                { name: '| Command Palette Settings', action: () => this.promptCommand('|') },
                { name: '\\ Escape first character', action: () => this.promptCommand('\\') },
                { name: '? Help', action: () => this.promptCommand('?') },
            ];
            this.showResults(searches);
        }

        filterProvider(terms, hint) {
            this.currentSelection = 0;
            this.hint.innerText = hint === undefined ? 'Filter operation' : hint;
            terms = '[' + terms;
            let fields = $tw.wiki.filterTiddlers('[fields[]]');
            let results = $tw.wiki.filterTiddlers(terms).map(r => { return { name: r } });
            let insertResult = (i, result) => results.splice(i + 1, 0, result);
            for (let i = 0; i < results.length; i++) {
                let initialResult = results[i];
                let alreadyMatched = false;
                let date = 'Invalid Date';
                if (initialResult.name.length === 17) { //to be sure to only match tiddly dates (17 char long)
                    date = $tw.utils.parseDate(initialResult.name).toLocaleString();
                }
                if (date !== "Invalid Date") {
                    results[i].hint = date;
                    results[i].action = () => { };
                    alreadyMatched = true;
                }
                let isTag = $tw.wiki.getTiddlersWithTag(initialResult.name).length !== 0;
                if (isTag) {
                    if (alreadyMatched) {
                        insertResult(i, { ...results[i] });
                        i += 1;
                    }
                    results[i].action = () => this.promptCommand('@' + initialResult.name);
                    results[i].hint = 'Tag'; //Todo more info?
                    alreadyMatched = true;
                }
                let isTiddler = this.tiddlerOrShadowExists(initialResult.name);
                if (isTiddler) {
                    if (alreadyMatched) {
                        insertResult(i, { ...results[i] });
                        i += 1;
                    }
                    results[i].action = () => { this.navigateTo(initialResult.name); this.closePalette() }
                    results[i].hint = 'Tiddler';
                    alreadyMatched = true;
                }
                let isField = fields.includes(initialResult.name);
                if (isField) {
                    if (alreadyMatched) {
                        insertResult(i, { ...results[i] });
                        i += 1;
                    }
                    let parsed;
                    try {
                        parsed = $tw.wiki.parseFilter(this.input.value)
                    } catch (e) { } //The error is already displayed to the user
                    let foundTitles = [];
                    for (let node of parsed || []) {
                        if (node.operators.length !== 2) continue;
                        if (node.operators[0].operator === 'title' && node.operators[1].operator === 'fields') {
                            foundTitles.push(node.operators[0].operand);
                        }
                    }
                    let hint = 'Field';
                    if (foundTitles.length === 1) {
                        hint = $tw.wiki.getTiddler(foundTitles[0]).fields[initialResult.name];
                        if (hint instanceof Date) {
                            hint = hint.toLocaleString();
                        }
                        hint = hint.toString().replace(/(\r\n|\n|\r)/gm, '');
                        let maxSize = this.settings.maxResultHintSize - 3;
                        if (hint.length > maxSize) {
                            hint = hint.substring(0, maxSize);
                            hint += '...';
                        }
                    }
                    results[i].hint = hint;
                    results[i].action = () => { };
                    alreadyMatched = true;
                }
                // let isContentType = terms.includes('content-type');
            }
            this.showResults(results);
        }

        filterResolver(e) {
            if (this.currentSelection === 0) return;
            this.currentResults[this.currentSelection - 1].result.action();
            e.stopPropagation();
        }

        helpResolver(e) {
            if (this.currentSelection === 0) return;
            this.currentResults[this.currentSelection - 1].result.action();
            e.stopPropagation();
        }

        createTiddlerProvider(terms) {
            this.currentSelection = 0;
            this.hint.innerText = 'Create new tiddler with title @tag(s)';
            this.showResults([]);
        }

        createTiddlerResolver(e) {
            let { tags, searchTerms } = this.parseTags(this.input.value.substr(1));
            let title = searchTerms.join(' ');
            tags = tags.join(' ');
            this.tmMessageBuilder('tm-new-tiddler', { title: title, tags: tags })(e);
            this.closePalette();
        }

        promptCommand(value, caret) {
            this.blockProviderChange = false;
            this.input.value = value;
            this.input.focus();
            if (caret !== undefined) {
                this.input.setSelectionRange(caret, caret);
            }
            this.onInput(this.input.value);
        }

        promptCommandBasic(value, caret, hint) {
            if (this.settings.neverBasic === 'true' || this.settings.neverBasic === true) { //TODO: validate settings to avoid unnecessary checks
                this.promptCommand(value, caret);
                return;
            }
            this.input.value = "";
            this.blockProviderChange = true;
            this.currentProvider = this.basicProviderBuilder(value, caret, hint);
            this.onInput(this.input.value);
        }

        basicProviderBuilder(value, caret, hint) {
            let start = value.substr(0, caret);
            let end = value.substr(caret);
            return (input) => {
                let { resolver, provider, terms } = this.parseCommand(start + input + end);
                let backgroundProvider = provider;
                backgroundProvider(terms, hint);
                this.currentResolver = resolver;
            }
        }

        getCommandHistory() {
            this.history = this.history.filter(h => this.actions.some(a => a.name === h)); //get rid of deleted command that are still in history;
            let results = this.history.map(h => this.actions.find(a => a.name === h));
            while (results.length <= this.settings.maxResults) {
                let nextDefaultAction = this.actions.find(a => !results.includes(a));
                if (nextDefaultAction === undefined)
                    break;
                results.push(nextDefaultAction);
            }
            return results;
        }
        actionResolver(e) {
            if (this.currentSelection === 0)
                return;
            let result = this.actions.find(a => a.name === this.currentResults[this.currentSelection - 1].innerText);
            if (result.keepPalette) {
                let curInput = this.input.value;
                this.goBack = () => {
                    this.input.value = curInput;
                    this.blockProviderChange = false;
                    this.onInput(this.input.value);
                };
            }
            this.updateCommandHistory(result);
            result.action(e);
            e.stopPropagation();
            if (result.immediate) {
                this.validateSelection(e);
                return;
            }
            if (!result.keepPalette) {
                this.closePalette();
            }
        }

        getCurrentSelection() {
            let selection = window.getSelection().toString();
            if (selection !== '') return selection;
            let activeElement = this.getActiveElement();
            if (activeElement === undefined || activeElement.selectionStart === undefined) return '';
            if (activeElement.selectionStart > activeElement.selectionEnd) {
                return activeElement.value.substring(activeElement.selectionStart, activeElement.selectionEnd);
            } else {
                return activeElement.value.substring(activeElement.selectionEnd, activeElement.selectionStart);
            }
        }
        getActiveElement(element = document.activeElement) {
            const shadowRoot = element.shadowRoot
            const contentDocument = element.contentDocument

            if (shadowRoot && shadowRoot.activeElement) {
                return this.getActiveElement(shadowRoot.activeElement)
            }

            if (contentDocument && contentDocument.activeElement) {
                return this.getActiveElement(contentDocument.activeElement)
            }

            return element
        }
        focusAtCaretPosition(el, caretPos) {
            if (el !== null) {
                el.value = el.value;
                // ^ this is used to not only get "focus", but
                // to make sure we don't have it everything -selected-
                // (it causes an issue in chrome, and having it doesn't hurt any other browser)
                if (el.createTextRange) {
                    var range = el.createTextRange();
                    range.move('character', caretPos);
                    range.select();
                    return true;
                }
                else {
                    // (el.selectionStart === 0 added for Firefox bug)
                    if (el.selectionStart || el.selectionStart === 0) {
                        el.focus();
                        el.setSelectionRange(caretPos, caretPos);
                        return true;
                    }

                    else { // fail city, fortunately this never happens (as far as I've tested) :)
                        el.focus();
                        return false;
                    }
                }
            }
        }
        createElement(name, proprieties, styles) {
            let el = this.document.createElement(name);
            for (let [propriety, value] of Object.entries(proprieties || {})) {
                el[propriety] = value;
            }
            for (let [style, value] of Object.entries(styles || {})) {
                el.style[style] = value;
            }
            return el;
        }
        /*
            Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
            */
        refresh() {
            return false;
        }
    }

    exports.commandpalettewidget = CommandPaletteWidget;

})();

in my case, I use the mousetrap plugin for single key (plus modifier) to invoke one of the three defined search types, very fast and convenient

...
    $tw.Mousetrap.bind("/",
        function() {
            $tw.rootWidget.invokeActionString('<$action-sendmessage $message="open-command-palette"/>',$tw.rootWidget);
        },
        "keyup"
    );
    $tw.Mousetrap.bind("shift+/",
        function() {
            $tw.rootWidget.invokeActionString('<$action-sendmessage $message="open-command-palette" $param="/"/>',$tw.rootWidget);
        },
        "keyup"
    );
    $tw.Mousetrap.bind("ctrl+/",
        function() {
            $tw.rootWidget.invokeActionString('<$action-sendmessage $message="open-command-palette" $param="~"/>',$tw.rootWidget);
        },
        "keyup"
    );
...
Souk21 commented 2 years ago

Hello, thanks for the suggestion, and sorry for the delay. If I understood correctly:

Let's say we want to recreate the "See Shadow Tiddlers" search, have a symbol for it (we're going to use ~ here, ie ~name should search for shadow tiddlers containing name) and have a global keyboard shortcut for it

First, we create a tiddler tagged $:/tags/CommandPaletteCommand with our query as content:

[all[shadows]search[]]

and the following fields:

command-palette-caret: 20
command-palette-name: See Shadows Tiddlers
command-palette-type: prompt-basic

(Note we need to specify where the search terms go in the query with the caret field)

Starting from v0.0.7, we can now add a 'prefix' field:

command-palette-prefix:~

The command palette will automatically search shadow tiddlers when the input starts with ~ Bonus: the symbol will show up in the help (help show up if you type ?)

Creating the shortcut is a native TW feature. We need to create a tiddler tagged $:/tags/KeyboardShortcut with an arbitrary key field, here we use 'cp-shadow':

key: ((cp-shadow))

and the following content

<$action-sendmessage $message="open-command-palette" $param="~"/>

(Note we pass our prefix ~ as $param)

Final step is to add a description of the shortcut. We create a tiddler named $:/config/ShortcutInfo/cp-shadow with a description as content. Here we use

Open the command palette and search for shadow tiddlers

Voila ! That should work. Let me know what you think :)

bepuzzled commented 2 years ago

Hi, and thanks for the response. Greatly appreciated.

The prefix is a good improvement, and very close to what I was after. The only downside with this approach is that I lose the richness of the multiple filter steps that is available with the regular search option. Do you have a recommended way to define multiple search steps for a custom search prefix?

(regarding the shortcut solution, agreed. However, I was primarily interested in a single-key shortcut (e.g. /) and that does not work well with the usual TW shortcut method because it fires even within text fields - not ideal. That is why I rely on the mousetrap plugin for those)