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();
})();