First off, thanks for this snippet! It has saved me tons of time…but I had a problem with it being really slow, so I modified the script to add in memory caching of the command list since that isn’t something that changes very often. This prevents it from having the super slow load time when you have alot of commands where the commands wouldn’t show up for a few seconds when I folded and unfolded the header where the dataview query was nested under. This also occurs when you go to a new tab and come back.
The optimized version that includes the in memory caching code, fixes this issue and it will only reload the query and pull data from the Obsidian command list every 10 minutes.
This is the code.
function Cache(config) {
config = config || {};
config.trim = config.trim || 600;
config.ttl = config.ttl || 3600;
var data = {};
var self = this;
var now = function() {
return new Date().getTime() / 1000;
};
/**
* Object for holding a value and an expiration time
* @param expires the expiry time as a UNIX timestamp
* @param value the value of the cache entry
* @constructor ¯\(°_o)/¯
*/
var CacheEntry = function(expires, value) {
this.expires = expires;
this.value = value;
};
/**
* Creates a new cache entry with the current time + ttl as the expiry.
* @param value the value to set in the entry
* @returns {CacheEntry} the cache entry object
*/
CacheEntry.create = function(value) {
return new CacheEntry(now() + config.ttl, value);
};
/**
* Returns an Array of all currently set keys.
* @returns {Array} cache keys
*/
this.keys = function() {
var keys = [];
for(var key in data)
if (data.hasOwnProperty(key))
keys.push(key);
return keys;
};
/**
* Checks if a key is currently set in the cache.
* @param key the key to look for
* @returns {boolean} true if set, false otherwise
*/
this.has = function(key) {
return data.hasOwnProperty(key);
};
/**
* Clears all cache entries.
*/
this.clear = function() {
for(var key in data)
if (data.hasOwnProperty(key))
self.remove(key);
};
/**
* Gets the cache entry for the given key.
* @param key the cache key
* @returns {*} the cache entry if set, or undefined otherwise
*/
this.get = function(key) {
return data[key].value;
};
/**
* Returns the cache entry if set, or a default value otherwise.
* @param key the key to retrieve
* @param def the default value to return if unset
* @returns {*} the cache entry if set, or the default value provided.
*/
this.getOrDefault = function(key, def) {
return self.has(key) ? data[key].value : def;
};
/**
* TODO: Add JSDoc to this function
*/
this.getOrCreate = function(key, fn) {
if (self.has(key)) {
return self.get(key);
}
const value = fn();
self.set(key, value);
return value;
}
/**
* Sets a cache entry with the provided key and value.
* @param key the key to set
* @param value the value to set
*/
this.set = function(key, value) {
data[key] = CacheEntry.create(value);
};
/**
* Removes the cache entry for the given key.
* @param key the key to remove
*/
this.remove = function(key) {
delete data[key];
};
/**
* Checks if the cache entry has expired.
* @param entrytime the cache entry expiry time
* @param curr (optional) the current time
* @returns {boolean} true if expired, false otherwise
*/
this.expired = function(entrytime, curr) {
if(!curr)
curr = now();
return entrytime < curr;
};
/**
* Trims the cache of expired keys. This function is run periodically (see config.ttl).
*/
this.trim = function() {
var curr = now();
for (var key in data)
if (data.hasOwnProperty(key))
if(self.expired(data[key].expires, curr))
self.remove(key);
};
// Periodical cleanup
setInterval(this.trim, config['trim'] * 1000);
//--------------------------------------------------------
// Events
var eventCallbacks = {};
this.on = function(event, callback) {
// TODO handle event callbacks
};
}
const cache = new Cache({ trim: 600, ttl: 3600 });
const getNestedObject = (nestedObj, pathArr) => {
const cacheKey = `nestedObject-${JSON.stringify({ nestedObj, pathArr })}`;
return cache.getOrCreate(cacheKey, () => {
return pathArr.reduce((obj, key) =>
(obj && obj[key] !== 'undefined') ? obj[key] : undefined, nestedObj);
});
}
function hilite(keys, how) {
if (keys && keys[1][0] !== undefined) {
return how + keys.flat(2).join('+').replace('Mod', 'Ctrl') + how;
} else {
return how + '–' + how;
}
}
function getHotkey(arr, highlight=true) {
const cacheKey = `hotkey-${JSON.stringify({ arr, highlight })}`;
return cache.getOrCreate(cacheKey, () => {
let hi = highlight ? '**' : '';
let defkeys = arr.hotkeys ? [[getNestedObject(arr.hotkeys, [0, 'modifiers'])],
[getNestedObject(arr.hotkeys, [0, 'key'])]] : undefined;
let ck = app.hotkeyManager.customKeys[arr.id];
var hotkeys = ck ? [[getNestedObject(ck, [0, 'modifiers'])], [getNestedObject(ck, [0, 'key'])]] : undefined;
return hotkeys ? hilite(hotkeys, hi) : hilite(defkeys, '');
});
}
let cmds = dv.array(Object.entries(app.commands.commands))
.sort(v => v[1].name, 'asc');
dv.paragraph(cmds.length + " commands currently enabled; " +
"non-default hotkeys <strong>bolded</strong>.<br><br>");
dv.table(["Command ID", "Name in current locale", "Hotkeys"],
cmds.map(v => [
v[1].id,
v[1].name,
getHotkey(v[1]),
])
);