Used Hotkeys – Grouped by Modifiers

A rough draft, to get a sense of which modifiers are least used, when choosing a new hotkey.

Sample listing:

⌘        Down F10 F11 F12 F9 JIS Left Return Right Section Up
        ' . / 0 1 5 7 9 ; B D E G I J K L N O R S T U W Y [ \ ]

⌥⌘        Right Up
        ' - . 0 1 2 3 4 5 6 7 8 9 ; = A B C D E F G H I J K L M N O P Q R S U V W X Z [ ]

⌃⌥⌘        Right
        , - / 1 2 3 A B C D E F G I L N O P Q R S T U V W X [ \ ]

⌥        Down F6 F8 Left Return Right Section Space Tab
        ' - . A B C F J L M P V [ ]

⌃        Tab
        , = C D G H J K N P T V

⌃⌘        C F H R V

⇧⌘        ' / 8 A D F K N O P Q V

⌃⌥        Down Left Right Space Up
        J P Q T V

⌃⇧        B C E J N S V

⌃⌥⇧⌘        Down Left Right Up
        B

⇧        JIS Return

⌥⇧        S T

⌥⇧⌘        / T

⌃⇧⌘        D

⌃⌥⇧        B

Used Hotkeys – grouped by modifiers.kmmacros (12 KB)


Expand disclosure triangle to view JS source
return (() => {
    "use strict";

    // Listing of used hotkeys, grouped by
    // modifier key combinations.

    // Rob Trew @2024

    const main = () =>
        sortBy(
            // Descending order of combinations used
            flip(comparing(hotkeyCount))
        )(
            groupOnKey(fst)(
                sortBy(
                    mappendComparing(
                        comparing(fst)
                    )(
                        comparing(snd)
                    )
                )(
                    usedCombinations()
                )
            )
        )
            .map(listingRow)
            .join("\n\n");


    // usedCombinations :: IO () -> [(String, String)]
    const usedCombinations = () => {
        const
            km = Application("Keyboard Maestro"),
            rgxKey = /([⇧⌃⌥⌘]+)([^\s]+)/u;

        return km.macros.triggers.where({
            description: {
                _beginsWith: "The Hot Key"
            }
        })
            .description()
            .flat()
            .flatMap(s => {
                const m = s.match(rgxKey);

                return m
                    ? [Tuple(m[1])(m[2])]
                    : [];
            })
    };


    // keysUsed :: [String] -> (String, String)
    const keysUsed = xs =>
        both(
            ks => Array.from(new Set(ks))
        )(
            partition(x => 1 < x.length)(xs)
        );


    // listingRow :: (String, [String]) -> String
    const listingRow = ([k, xs]) => {
        const
            usedRows = [...keysUsed(xs.map(snd))]
                .flatMap(
                    xs => 0 < xs.length
                        ? [unwords(xs)]
                        : []
                )
                .join("\n\t\t");

        return `${k}\t\t${usedRows}`;
    };

    // hotkeyCount :: (String, [String]) -> Int
    const hotkeyCount = pair =>
        pair[1].length

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

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


    // both :: (a -> b) -> (a, a) -> (b, b)
    const both = f =>
        // A tuple obtained by separately
        // applying f to each of the two
        // values in the given tuple.
        ([a, b]) => Tuple(
            f(a)
        )(
            f(b)
        );


    // comparing :: Ord a => (b -> a) -> b -> b -> Ordering
    const comparing = f =>
        // The ordering of f(x) and f(y) as a value
        // drawn from {-1, 0, 1}, representing {LT, EQ, GT}.
        x => y => {
            const
                a = f(x),
                b = f(y);

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


    // eq (==) :: Eq a => a -> a -> Bool
    const eq = a =>
        // True when a and b are equivalent in the terms
        // defined below for their shared data type.
        b => {
            const t = typeof a;

            return t === typeof b && (
                "object" !== t
                    ? "function" !== t
                        ? a === b
                        : a.toString() === b.toString()
                    : (() => {
                        const kvs = Object.entries(a);

                        return kvs.length !== Object.keys(b).length
                            ? false
                            : kvs.every(([k, v]) => eq(v)(b[k]));
                    })()
            );
        };


    // first :: (a -> b) -> ((a, c) -> (b, c))
    const first = f =>
        // A simple function lifted to one which applies
        // to a tuple, transforming only its first item.
        ([x, y]) => Tuple(f(x))(y);


    // 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)
            : (a => b => op(b)(a));


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


    // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
    const groupBy = eqOp =>
        // A list of lists, each containing only elements
        // equal under the given equality operator, such
        // that the concatenation of these lists is xs.
        xs => 0 < xs.length
            ? (() => {
                const [h, ...t] = xs;
                const [groups, g] = t.reduce(
                    ([gs, a], x) => eqOp(a[0])(x)
                        ? [gs, [...a, x]]
                        : [[...gs, a], [x]],
                    [[], [h]]
                );

                return [...groups, g];
            })()
            : [];


    // groupOnKey :: Eq k => (a -> k) -> [a] -> [(k, [a])]
    const groupOnKey = f =>
        // A list of (k, [a]) tuples, in which each [a]
        // contains only elements for which f returns the
        // same value, and in which k is that value.
        // The concatenation of the [a] in each tuple === xs.
        xs => 0 < xs.length
            ? groupBy(a => b => a[0] === b[0])(
                xs.map(x => [f(x), x])
            )
                .map(gp => [
                    gp[0][0],
                    gp.map(ab => ab[1])
                ])
            : [];


    // mappendComparing (<>) :: (a -> a -> Bool)
    // (a -> a -> Bool) -> (a -> a -> Bool)
    const mappendComparing = cmp =>
        cmp1 => a => b => {
            const x = cmp(a)(b);

            return 0 !== x
                ? x
                : cmp1(a)(b);
        };


    // partition :: (a -> Bool) -> [a] -> ([a], [a])
    const partition = p =>
        // A tuple of two lists - those elements in
        // xs which match p, and those which do not.
        xs => [...xs].reduce(
            (a, x) => (
                p(x)
                    ? first
                    : second
            )(ys => [...ys, x])(a),
            Tuple([])([])
        );


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


    // second :: (a -> b) -> ((c, a) -> (c, b))
    const second = f =>
        // A function over a simple value lifted
        // to a function over a tuple.
        // f (a, b) -> (a, f(b))
        xy => Tuple(
            xy[0]
        )(
            f(xy[1])
        );


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        // A copy of xs sorted by the comparator function f.
        xs => xs.slice()
            .sort((a, b) => f(a)(b));


    // sortOn :: Ord b => (a -> b) -> [a] -> [a]
    const sortOn = f =>
        // Equivalent to sortBy(comparing(f)), but with f(x)
        // evaluated only once for each x in xs.
        // ('Schwartzian' decorate-sort-undecorate).
        xs => sortBy(
            comparing(x => x[0])
        )(
            xs.map(x => [f(x), x])
        )
            .map(x => x[1]);

    // unwords :: [String] -> String
    const unwords = xs =>
        // A space-separated string derived
        // from a list of words.
        xs.join(" ");


    // showLog :: a -> IO ()
    const showLog = (...args) =>
        // eslint-disable-next-line no-console
        console.log(
            args
                .map(JSON.stringify)
                .join(" -> ")
        );


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

This is super handy, thanks for sharing!

1 Like