Listing KM Variables by Descending Size (And Optionally Clearing Some)

A quick piece of JavaScript for Applications:

  1. See which variables contain the largest amount of material
  2. Optionally select and clear a few variables.

Moderator Edit:
==Macro UPDATED== below by author: 2021-02-06


(See, for example, the need to trim down which was reported here:

The script below takes a few seconds to display the size-sorted list of KM variables, so if you paste it into (Yosemite+) Script Editor and run it from there, it displays a little progress pie-chart at the bottom of the script window.

(If you save it from Script Editor as an .app, and launch the application,

it will display a progress bar.

You can then, if you want, select any particularly large variables and click OK to empty their contents.

Source code:

(function () {

    // LIST KEYBOARD MAESTRO VARIABLES
// SORTED BY DESCENDING SIZE
// OPTIONALLY EMPTY THE CONTENTS OF SELECTED VARIABLES 

// CAUTION: NOT UNDOABLE

/* * *
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
* * */

var kme = Application("Keyboard Maestro Engine"),
    vars = kme.variables(),
    lngVars = vars.length,
    lngHalf = Math.floor(lngVars / 2);

Progress.totalUnitCount = lngVars;
Progress.description = 'Sizing KM Engine variables';
Progress.additionalDescription = 'This may take a little while';


var lstSized = vars.map(
    function (x, i) {

        Progress.completedUnitCount = i / 2;

        return {
            name: x.name(),
            size: x.value().length
        };
    }

);

Progress.description = 'Sorting KM Engine variables';

lstSized.sort(
    function (a, b) {
        return b.size - a.size;
    }
);

Progress.description = 'Listing KM Engine variables';

var lstReport = lstSized.map(
    function (dct, i) {
        Progress.completedUnitCount = lngHalf + (i / 2);
        return dct.name + '\t' + dct.size;
    }
);

var a = Application.currentApplication(),
    sa = (a.includeStandardAdditions = true, a);

var strClip = lstReport.join('\n');

sa.setTheClipboardTo(strClip);

sa.activate();
var varChoice = sa.chooseFromList(lstReport, {
        withTitle: 'KME variable sizes',
        withPrompt: 'Clear selected variables:',
        okButtonName: 'Clear contents of selected variables',
        cancelButtonName: 'Cancel',
        multipleSelectionsAllowed: true,
        emptySelectionAllowed: true
    }),

    lstChoice = varChoice ? varChoice : [];

if (lstChoice.length) {
    var strNames = '';
    lstChoice.forEach(
        function (s) {
            lstParts = s.split(/\t/);
            kme.variables[lstParts[0]].value = '';
            strNames += lstParts[0] + '  ';
        }
    );
    sa.activate()
    sa.displayNotification(strNames, {
        withTitle: "Cleared selected KM variables:"
    })
}
})();
7 Likes

Hey Rob,

Cool!

I’d make one little change and remove default items to make accidents less likely.

I hit Return by reflex and cleared a variable without meaning to.

-Chris

Good thought – edited above.

1 Like

Really nice. Thanks for this.

An updated version – slightly tidier menu, a bit easier to maintain, and possibly, FWIW, fractionally faster:

(() => {
    // LIST KEYBOARD MAESTRO VARIABLES
    // SORTED BY DESCENDING SIZE
    // OPTIONALLY EMPTY THE CONTENTS OF SELECTED VARIABLES 

    // CAUTION: NOT UNDOABLE

    // Ver 0.2

    /* * *
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.
    * * */

    ObjC.import('AppKit');

    // main :: IO ()
    const main = () => {
        const
            kmVars = Application(
                "Keyboard Maestro Engine"
            ).variables

        // ------- REPORT ON ANY CLEARED VARIABLES -------
        return clearanceReport(kmVars)(
            
            // ----- ANY KM VARS CHOSEN FROM A MENU ------
            chosenKMVars(
                listingBySize(
                    sortedBySize(
                        kmVarsWithSizes(kmVars)
                    )
                )
            )
        );
    };

    // chosenKMVars = [{name::String, size:Int}] -> IO [String]
    const chosenKMVars = reportLines => {
        const
            sa = Object.assign(Application.currentApplication(), {
                includeStandardAdditions: true
            });
        return (
            // Outside JS
            copyText(reportLines.join('\n')),
            sa.activate(),
            // and within JS.
            sa.chooseFromList(
                reportLines, {
                    withTitle: 'KME variable sizes',
                    withPrompt: 'Clear selected variables:',
                    okButtonName: 'Clear contents of selected variables',
                    cancelButtonName: 'Cancel',
                    // defaultItems: [reportLines[0]], 
                    multipleSelectionsAllowed: true,
                    emptySelectionAllowed: true
                }) || []
        );
    };


    // clearanceReport :: KM Variables -> IO String
    const clearanceReport = kmVars =>
        choices => 0 < choices.length ? (() => {
            const
                namesOfClearedKMVars = choices.map(s => {
                    const
                        parts = s.split(/\t/),
                        name = parts[1];
                    return (
                        // Outside JS,
                        kmVars.byName(name).value = '',
                        // and within JS.
                        name
                    );
                }).join(', '),
                sa = Object.assign(
                    Application.currentApplication(), {
                        includeStandardAdditions: true
                    });
            return (
                // Outside JS,
                sa.activate(),
                sa.displayNotification(namesOfClearedKMVars, {
                    withTitle: "Cleared selected KM variables:"
                }),
                // and within JS.
                namesOfClearedKMVars
            );
        })() : 'No variables cleared.';


    // kmVarsWithSizes :: KM Variables -> IO [{name::String, size::Int}]
    const kmVarsWithSizes = kmVars => (
        // Outside JS,
        Progress.totalUnitCount = kmVars.length,
        Progress.description = 'Sizing KM Engine variables',
        Progress.additionalDescription = 'This may take a little while',
        // and within JS.
        kmVars().map(
            (x, i) => (
                Progress.completedUnitCount = i / 2, {
                    name: x.name(),
                    size: x.value().length
                }
            )
        )
    );

    // listingBySize :: [{name::String, size::Int}] -> IO [String]
    const listingBySize = sortedVars => {
        const
            intHalf = Math.floor(sortedVars.length / 2),
            maxWidth = str(maximum(
                sortedVars.map(x => x.size)
            )).length;
        return (
            // Outside JS,
            Progress.description = 'Listing KM Engine variables',
            // and within JS.
            sortedVars.map(
                (dct, i) => (
                    // Outside JS,
                    Progress.completedUnitCount = intHalf + (i / 2),
                    // and within JS.
                    `${justifyRight(maxWidth)('0')(str(dct.size))}\t${dct.name}`
                )
            )
        );
    };

    // sortedBySize :: [{name::String, size::Int}] -> 
    // IO [{name::String, size::Int}]
    const sortedBySize = sizedVars => (
        // Outside JS,
        Progress.description = 'Sorting KM Engine variables',
        // and within JS.
        sortBy(
            flip(comparing(x => x.size))
        )(
            sizedVars
        )
    );

    // ----------------------- JXA -----------------------

    // copyText :: String -> IO String
    const copyText = s => {
        const pb = $.NSPasteboard.generalPasteboard;
        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };

    // --------------------- GENERIC ---------------------

    // flip :: (a -> b -> c) -> b -> a -> c
    const flip = op =>
        // The binary function op with 
        // its arguments reversed.
        x => y => op(y)(x);


    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        x => y => {
            const
                a = f(x),
                b = f(y);
            return a < b ? -1 : (a > b ? 1 : 0);
        };


    // justifyRight :: Int -> Char -> String -> String
    const justifyRight = n =>
        // The string s, preceded by enough padding (with
        // the character c) to reach the string length n.
        c => s => n > s.length ? (
            s.padStart(n, c)
        ) : s;


    // maximum :: Ord a => [a] -> a
    const maximum = xs => (
        // The largest value in a non-empty list.
        ys => 0 < ys.length ? (
            ys.slice(1).reduce(
                (a, y) => y > a ? (
                    y
                ) : a, ys[0]
            )
        ) : undefined
    )(xs);


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        xs => xs.slice()
        .sort((a, b) => f(a)(b));


    // str :: a -> String
    const str = x =>
        Array.isArray(x) && x.every(
            v => ('string' === typeof v) && (1 === v.length)
        ) ? (
            x.join('')
        ) : x.toString();

    // MAIN --
    return main();
})();

Ver 0.4

Significantly faster menu of KM Variables by descending size.

List KM variables by size, and offer deletion of a selection.kmmacros (27.0 KB)

JS Source
(() => {
    "use strict";

    // LIST KEYBOARD MAESTRO VARIABLES
    // SORTED BY DESCENDING SIZE
    // OPTIONALLY EMPTY THE CONTENTS OF SELECTED VARIABLES

    // CAUTION: NOT UNDOABLE

    // Ver 0.4

    /* * *
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
    ANY KIND, EXPRESS ORIMPLIED, INCLUDING BUT NOT LIMITED
    TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A
    PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
    SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
    ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
    ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
    OR OTHER DEALINGS IN THE SOFTWARE.
    * * */

    // main :: IO ()
    const main = () =>
        either(
            message => "User cancelled" !== message ? (
                alert("KM Variables")(message)
            ) : message
        )(
            alert("Deleted")
        )(
            bindLR(
                menuOfKMVariablesLR(
                    kmVarsBySize()
                )
            )(
                selectedStrings => deletedWhereConfirmedLR(
                    selectedStrings
                )
            )
        );


    // deletedWhereConfirmedLR :: [String] -> Either String String
    const deletedWhereConfirmedLR = ks => {
        const
            kmVarNames = ks.map(k => k.split(/\t/u)[1]),
            indentedListing = `\t${kmVarNames.join("\n\t")}`;

        return "Delete" !== confirm("Selected KM variables")(
            `Delete:\n\n${indentedListing}`
        )("Delete") ? (
            Left("User cancelled")
        ) : (() => {
            const
                kme = Application("Keyboard Maestro Engine");

            return (
                kmVarNames.forEach(k => {
                    kme.setvariable(k, {
                        to: ""
                    });
                }),
                Right(indentedListing)
            );
        })();
    };

    // kmVarsBySize :: () -> [(String, Int)]
    const kmVarsBySize = () => {
        const
            kme = Application("Keyboard Maestro Engine"),
            kmVars = kme.variables;

        return sortBy(
            flip(comparing(snd))
        )(
            zipWith(
                k => v => Tuple(k)(v.length)
            )(
                kmVars.name()
            )(
                kmVars.value()
            )
        );
    };


    // menuOfKMVariablesLR :: [(String, Int)] ->
    // IO Either String String
    const menuOfKMVariablesLR = varsBySize => {
        const colWidth = varsBySize[0][1].toString().length;

        return showMenuLR(true)(
            `${varsBySize.length} ` + (
                "KM variables by descending size"
            )
        )(
            "⌘-Click for multiple selections"
        )(
            varsBySize.map(
                kv => {
                    const
                        valueString = justifyRight(
                            colWidth
                        )(" ")(snd(kv).toString());

                    return `${valueString}\t${fst(kv)}`;
                }
            )
        )([]);
    };

    // ----------------------- JXA -----------------------

    // alert :: String -> String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };

    // confirm :: String -> String -> String -> IO String
    const confirm = title =>
        prompt => buttonName => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            sa.activate();
            try {
                return sa.displayDialog(prompt, {
                    withTitle: title,
                    buttons: ["Cancel", buttonName],
                    defaultButton: "Cancel"
                }).buttonReturned;
            } catch (e) {
                return "Cancel";
            }
        };

    // showMenuLR :: Bool -> String -> String ->
    // [String] -> String -> Either String [String]
    const showMenuLR = blnMult =>
        // An optionally multi-choice menu, with
        // a given title and prompt string.
        // Listing the strings in xs, with
        // the the string `selected` pre-selected
        // if found in xs.
        title => prompt => xs =>
        selected => 0 < xs.length ? (() => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            sa.activate();
            const v = sa.chooseFromList(xs, {
                withTitle: title,
                withPrompt: prompt,
                defaultItems: xs.includes(selected) ? (
                    []
                ) : [],
                okButtonName: "OK",
                cancelButtonName: "Cancel",
                multipleSelectionsAllowed: blnMult,
                emptySelectionAllowed: false
            });

            return Array.isArray(v) ? (
                Right(v)
            ) : Left("User cancelled");
        })() : Left(`${title}: No items to choose from.`);


    // --------------------- GENERIC ---------------------

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2
        });


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => undefined !== m.Left ? (
            m
        ) : mf(m.Right);


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => "Either" === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;


    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        x => y => {
            const
                a = f(x),
                b = f(y);

            return a < b ? -1 : (a > b ? 1 : 0);
        };


    // flip :: (a -> b -> c) -> b -> a -> c
    const flip = op =>
        // The binary function op with
        // its arguments reversed.
        1 < op.length ? (
            (a, b) => op(b, a)
        ) : (x => y => op(y)(x));


    // fst :: (a, b) -> a
    const fst = tpl =>
        // First member of a pair.
        tpl[0];


    // justifyRight :: Int -> Char -> String -> String
    const justifyRight = n =>
        // The string s, preceded by enough padding (with
        // the character c) to reach the string length n.
        c => s => n > s.length ? (
            s.padStart(n, c)
        ) : s;


    // list :: StringOrArrayLike b => b -> [a]
    const list = xs =>
        // xs itself, if it is an Array,
        // or an Array derived from xs.
        Array.isArray(xs) ? (
            xs
        ) : Array.from(xs || []);


    // snd :: (a, b) -> b
    const snd = tpl =>
        // Second member of a pair.
        tpl[1];


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        xs => list(xs).slice()
        .sort((a, b) => f(a)(b));


    // zipWith :: [a] -> [b] -> [(a, b)]
    const zipWith = f =>
        // The paired members of xs and ys, up to
        // the length of the shorter of the two lists.
        xs => ys => Array.from({
            length: Math.min(xs.length, ys.length)
        }, (_, i) => f(xs[i])(ys[i]));

    // MAIN ---
    return main();
})();
4 Likes

nice!
is there a way to just open the macro with the large variable? (instead of deleting it)

Open ? Tell me more, these are variables, rather than macros, but would like to inspect their contents ?

I think I may not yet have caught up with the macro with the large variable – it sounds as if variables might be tied to particular macros – so I think I'm probably being slow :slight_smile:

sorry, should have been clearer.
But now notice my idea won't fly. Because as you mentioned, these are variables, so they might appear/be used in more than one place.

but the macro as it is has really helped; found a couple variables with a humongous amount of text in them. So, searched for them, and cleared them up to a more manageable level.

:+1:

1 Like

@ComplexPoint,

Thanks for sharing your script and macro. It now runs very fast, and I think it is very useful.

I would like to modify your JXA script, but I can't understand it well enough to do so.
Maybe you could suggest where in your script, and how I might make changes to provide:

  1. Display of selected variables
  2. Save of selected variables to file (one file per variable).
  3. As Chris suggested, show the initial variable list without any default selection.

Perhaps if you could mod your script to provide buttons on the confirm dialog for the above, and a stub function for each, I can then provide the actual functions.

Here's a prototype of the dialog I would like:

image

Thanks for your help.

BTW, I have added this Macro and one of your other macros to the Best Macro List.

P.S. If anyone can determine how to mod @ComplexPoint's script to do the above, please feel free to post.

  1. As Chris suggested, show the initial variable list without any default selection.

Thanks – I've modified that and refreshed the macro and source listing (ver 0.4) in the original post.

(Essentially, in showMenuLR, the value of defaultItems: needs to be an empty [ ])

changes to provide (etc etc)

You'll see in the new listing (0.4) that I've expanded the lower expression of the main() function from its reduced version:

deletedWhereConfirmedLR

to one with an explicit argument:

selectedStrings => deletedWhereConfirmedLR(
    selectedStrings
)

What you need to do is to provide a set of alternatives to the deletedWhereConfirmedLR function, and offer a UI choice between them.

Your implementations of functions with names perhaps like:

selectionsSavedToFileLR

and

selectionValuesDisplayedLR
  • Always need to return a value of some kind (no bombing out into an error)
  • and that value must be wrapped either in a Right(...) function call, or in a Left(...) function call.

Right is the channel for values returned by a successful computation, eg

Right(`Saved to: ${filePath}`)

or

Right(kmVarNames.join("\n"))

and Left(message) is the channel for explanations of what was cancelled or not feasible.


For a composable (value-returning) wrapper around a UI loop you could use:

// until :: (a -> Bool) -> (a -> a) -> a -> a
const until = p =>
    // The value resulting from repeated applications
    // of f to the seed value x, terminating when
    // that result returns true for the predicate p.
    f => x => {
        let v = x;

        while (!p(v)) {
            v = f(v);
        }

        return v;
    };

from GitHub - RobTrew/prelude-jxa: Generic functions for macOS and iOS scripting in Javascript – function names as in Hoogle

1 Like

Thanks for your help and guidance.
I'll give it a go, but I may have more questions. :wink: