Obsidian commands, hotkeys and duplicate hotkey assignments

The script presented below will list all available commands, default and custom hotkeys, and keys associated with multiple commands. Through changing variables at the front of the script, one can also fine tune to show various columns, with/without keys, and exclude or only include specific modules.

As it’s written here, it’ll show commands which have one or more keys bound to that command, and it’s sorted by the key (before modifiers), so that Cmd P is shown before Cmd W. (This related to the first key assigned to that command). The duplicates if any are shown below, and if no duplicates exists that is also listed).

To use the script, simply copy it and insert into a note of your choice. Change variables at the first part, if you want to adapt it to personal preferences.

The script

```dataviewjs
//// Optionally show duplicate keys
const showDuplicatedHotkeys = true


//// Define variations for the main table
let idColumn = true
const splitIdColumn = true   // Will affect idColumn
const nameColumn = true
let keysColumn = true
// Select if subsets are shown or not
const showDefaultKeys = true
const showCustomKeys = true
const showCommandsWithoutKeys = false
// Don't show keycolumn if no keys are actually shown
keysColumn = keysColumn && (showDefaultKeys || showCustomKeys)
const highlightCustomKeys = true
const useSymbolsForModifiers = false  // Adapted for Mac symbols

/**********************
 * Sorting options:  *
   "id"       // module + cmdname
   "cmdname"  // only cmdname
   "fullname" // full descriptive name
   "keycount" // Keys defined for this command
   "keys"     // "character + modifiers" for first command
**********************/
const sort = "keys"
const ascending = true

// Module/plugin options
const showAllModules = true  // With exception of those in excludedModules
let excludeModules = [
  'exclude-this-plugin',
] // Note it doesn't matter if there is a trailing comma in
  // either of these lists. So it's better to keep them in there
  // Add a line like above, for each plugin you want excluded 

// If not showing all modules, list the following (NB! Don't exclude any)
let includeModules = [
]

/********************************************
 * Some of the default modules in Obsidian **
  'editor',
  'app',
  'workspace',
  'theme',
  'markdown',
  'file-explorer',
  'window',
  'graph',
  'backlink',
  'canvas',
  'starred',
  'open-with-default-app',
  'global-search',
  'switcher',
  'outgoing-links',
  'daily-notes',
  'tag-pane',
  'note-composer',
  'command-palette',
  'outline',
  'file-recovery',
 *******************************************/


// Don't show idColumn if also splitting the ID, to avoid redundancy
idColumn = idColumn && !splitIdColumn


/******************************************************/
/** Collate lists of commands with keys              **/
/******************************************************/

let cmds = {}  // Holds all commands with keys used
let keys = {}  // Using the key combinations as key, list all assigned commands 

dv.array(Object.values(app.commands.commands))
  .forEach(cmd => {
    cmds[cmd.id] = { 
      id: cmd.id,
      moduleId: "",
      moduleCmdName: "", 
      name: cmd.name,
      defKeys: [],
      custKeys: [],
      keyCount: 0
    }

    const [ sId, sName ] = cmd.id.split(":")
    cmds[cmd.id].moduleId = sId
    cmds[cmd.id].moduleCmdName = sName
    
    // Collate default hotkeys for this command
    if ( cmd.hotkeys && cmd.hotkeys.length > 0 ) 
      cmds[cmd.id].defKeys = cmd.hotkeys
   
    // Collate custom hotkeys for this command
    let customKeys = app.hotkeyManager.customKeys[cmd.id]
    if ( customKeys && customKeys.length > 0 ) {
      cmds[cmd.id].defKeys = [] // Custom hotkeys override default keys
      cmds[cmd.id].custKeys = customKeys
    }

    // Add both type of keys to the keys-variable
    if ( cmds[cmd.id].defKeys.length > 0 ||
         cmds[cmd.id].custKeys.length > 0 )
      for (let aKey of [ ...cmds[cmd.id].defKeys, ...cmds[cmd.id].custKeys] ) {
        const keyCombo = joinModifiers(aKey.modifiers) + aKey.key
        if ( !( keyCombo in keys ) ) 
          keys[ keyCombo ]  = []

        keys[ keyCombo ].push(cmd.id)
        cmds[cmd.id].keyCount += 1
      }      
  }) 

/******************************************************/
/** Build the main table                             **/
/******************************************************/

// Build headers list
let headers = []
if (idColumn) headers.push("Command ID")
if (splitIdColumn) {
   headers.push("Module ID")
   headers.push("Module CmdName")
}
if (nameColumn) headers.push("Command name")
if (keysColumn) headers.push("Keys")


// Output the table
dv.table(headers,
    dv.array(Object.values(cmds))
 
      .filter(cmd => 
        (showDefaultKeys && cmd.defKeys.length > 0) ||
        (showCustomKeys && cmd.custKeys.length > 0 ) ||
        (showCommandsWithoutKeys && cmd.keyCount == 0) )
      .filter(cmd => {
        if ( showAllModules )
           return !excludeModules.includes(cmd.moduleId)
        else 
           return includeModules.includes(cmd.moduleId)
      })
      .sort( cmd => {
        if ( sort == "fullname" ) 
          return cmd.name
        if ( sort == "cmdname" )
          return cmd.moduleCmdName
        if ( sort == "keycount" )
          return cmd.keyCount
        if ( sort == "keys" )
          return sortkeys(cmds[cmd.id].defKeys, cmds[cmd.id].custKeys)
          
        return cmd.id }, ascending ? "asc" : "desc"
      ) /* */.map(cmd => {
        let columns = []
        if (idColumn) columns.push(cmd.id)
        if (splitIdColumn) {
          columns.push(cmd.moduleId)
          columns.push(cmd.moduleCmdName)
        }
        if (nameColumn) columns.push(cmd.name) 
        if (keysColumn) columns.push(hotkeys(cmd.defKeys, cmd.custKeys))
        return columns
      })
    ) // for dv.table


/**********************
 * Sorting options:  *
   "cmdId"
   "cmdName"
   "moduleIdAndName"
   "moduleCmdName"
   "keys"
**********************/

/******************************************************/
/** Code to show potential duplicated keys           **/
/******************************************************/
	
if ( showDuplicatedHotkeys ) {
  const duplicates = dv.array( Object.entries(keys) )
    .filter(k => k[1].length > 1)

  if ( duplicates.length > 0 ) {
    dv.header(2, "Duplicates exists")
    dv.table(["Key", "Command ID", "Command name"],
      duplicates
        .map(k => [k[0], k[1], k[1].map(i => cmds[i].name) ]) 
    )
  } else {
    dv.paragraph("No keys are duplicated")
  }
}



/******************************************************/
/** Some helper function                             **/
/******************************************************/
// Sort modifiers, and provide symbols if wanted
// NB! Needs changes if used on Windows, I reckon
function joinModifiers(modifiers) {
  const symbols = useSymbolsForModifiers
  return modifiers.sort((a, b) => (a < b ? -1 : 1)).join(' ')
	              .replace('Mod', symbols ? '⌘' : 'CMD')
    	          .replace('Alt', symbols ? '⌥' : 'OPT')
    	          .replace('Ctrl', symbols ? '⌃' : 'CTRL')
    	          .replace('Shift', symbols ? '⇧' : 'SHIFT') +
    	          (modifiers.length > 0 ? ' ' : '')
}


function hotkeys(defKeys, custKeys) {
  let keys = []
  if ( showDefaultKeys )
    for (let aKey of defKeys)
      keys.push( joinModifiers(aKey.modifiers) + (aKey.key == ' ' ? 'Space' : aKey.key ) )

  if ( showCustomKeys )
    for (let aKey of custKeys)
      keys.push(
        ( highlightCustomKeys ? "==" : "" ) +         
        joinModifiers(aKey.modifiers) + (aKey.key == ' ' ? 'Space' : aKey.key ) +
        ( highlightCustomKeys ? "==" : "" ) 
    )

  return keys.join("<br />")
}

function sortkeys(defKeys, custKeys) {
  const sortlist = []
  if ( showDefaultKeys ) 
    sortlist.push(...defKeys)

  if ( showCustomKeys )
    sortlist.push(...custKeys)

  return sortlist.map(m => m.key + joinModifiers(m.modifiers)).join(":")
}
```

Output from my vault with some custom hotkeys set:

Credits

Moonbase59 revised version of his own script:

My modification of it:

PS! I’ve opted to post this as a new post, instead of a reply in the Dataview JS Snippets as I see requests every now and then related to this subject, and hopefully this post will answer most questions related to duplicate key settings, and how to resolve them or locate commands and so on.

6 Likes

this is great @holroy. Thank you sharing this.

I have a request - I’m looking to identify only modified keys. But if I use only the showCustomKeys = true, the result doesn’t include commands where the default hotkey has been removed and it doesn’t have a custom key.

Is there a way to identify these commands?

I don’t think that’s possible to identify. That is if they’re now set to blank, but was something before.

1 Like

This is great – many thanks for sharing. Do you, or anyone else that may be watching – know how to adapt this for Windows?

The script will work for windows, you just need to adjust the text or symbols towards the end of the script to the windows standards.

And if someone knows how to identify that we are in windows within the script I can make that change to the main script, as well, so that the texts are correct out of the box.

Got it! Ta.

Thank you @holroy . I fixed a hotkey conflict that has been bothering me for ages. Very much appreciated.

2 Likes