diff --git a/src/cloud/components/Application.tsx b/src/cloud/components/Application.tsx
index fbaba6f71f..59821c8034 100644
--- a/src/cloud/components/Application.tsx
+++ b/src/cloud/components/Application.tsx
@@ -9,7 +9,6 @@ import {
 import { isActiveElementAnInput, InputableDomElement } from '../lib/dom'
 import { useEffectOnce } from 'react-use'
 import { useSettings } from '../lib/stores/settings'
-import { isPageSearchShortcut, isSidebarToggleShortcut } from '../lib/shortcuts'
 import { useSearch } from '../lib/stores/search'
 import AnnouncementAlert from './AnnouncementAlert'
 import {
@@ -20,6 +19,8 @@ import {
   newDocEventEmitter,
   switchSpaceEventEmitter,
   SwitchSpaceEventDetails,
+  togglePreviewModeEventEmitter,
+  toggleSplitEditModeEventEmitter,
 } from '../lib/utils/events'
 import { usePathnameChangeEffect, useRouter } from '../lib/router'
 import { useNav } from '../lib/stores/nav'
@@ -76,6 +77,7 @@ import {
 } from './molecules/PageSearch/InPageSearchPortal'
 import SidebarToggleButton from './SidebarToggleButton'
 import { getTeamURL } from '../lib/utils/patterns'
+import { compareEventKeyWithKeymap } from '../../lib/keymap'
 
 interface ApplicationProps {
   className?: string
@@ -229,11 +231,47 @@ const Application = ({
         return
       }
 
-      if (isSidebarToggleShortcut(event)) {
-        preventKeyboardEventPropagation(event)
-        setPreferences((prev) => {
-          return { sidebarIsHidden: !prev.sidebarIsHidden }
-        })
+      const keymap = preferences['keymap']
+      if (keymap != null) {
+        const sidenavToggleShortcut = keymap.get('toggleSideNav')
+        if (compareEventKeyWithKeymap(sidenavToggleShortcut, event)) {
+          preventKeyboardEventPropagation(event)
+          setPreferences((prev) => {
+            return { sidebarIsHidden: !prev.sidebarIsHidden }
+          })
+        }
+      }
+
+      if (!usingElectron && keymap != null) {
+        const toggleGlobalSearchShortcut = keymap.get('toggleGlobalSearch')
+        if (compareEventKeyWithKeymap(toggleGlobalSearchShortcut, event)) {
+          preventKeyboardEventPropagation(event)
+          searchEventEmitter.dispatch()
+        }
+      }
+
+      if (!usingElectron && keymap != null) {
+        const openPreferencesShortcut = keymap.get('openPreferences')
+        if (compareEventKeyWithKeymap(openPreferencesShortcut, event)) {
+          preventKeyboardEventPropagation(event)
+          openSettingsTab('preferences')
+        }
+      }
+
+      if (!usingElectron && keymap != null) {
+        const togglePreviewModeShortcut = keymap.get('togglePreviewMode')
+        if (compareEventKeyWithKeymap(togglePreviewModeShortcut, event)) {
+          preventKeyboardEventPropagation(event)
+          togglePreviewModeEventEmitter.dispatch()
+        }
+      }
+
+      if (!usingElectron && keymap != null) {
+        const toggleSplitEditModeShortcut = keymap.get('toggleSplitEditMode')
+        if (compareEventKeyWithKeymap(toggleSplitEditModeShortcut, event)) {
+          preventKeyboardEventPropagation(event)
+          toggleSplitEditModeEventEmitter.dispatch()
+        }
       }
 
       if (isSingleKeyEvent(event, 'escape') && isActiveElementAnInput()) {
@@ -244,17 +282,20 @@ const Application = ({
         ;(document.activeElement as InputableDomElement).blur()
       }
 
-      if (usingElectron && isPageSearchShortcut(event)) {
-        preventKeyboardEventPropagation(event)
-        if (showInPageSearch) {
-          setShowInPageSearch(false)
-          setShowInPageSearch(true)
-        } else {
-          setShowInPageSearch(true)
+      if (usingElectron && keymap != null) {
+        const inPageSearchShortcut = keymap.get('toggleInPageSearch')
+        if (compareEventKeyWithKeymap(inPageSearchShortcut, event)) {
+          preventKeyboardEventPropagation(event)
+          if (showInPageSearch) {
+            setShowInPageSearch(false)
+            setShowInPageSearch(true)
+          } else {
+            setShowInPageSearch(true)
+          }
         }
       }
     },
-    [team, setPreferences, showInPageSearch]
+    [team, preferences, setPreferences, openSettingsTab, showInPageSearch]
   )
   useGlobalKeyDownHandler(overrideBrowserCtrlsHandler)
 
diff --git a/src/cloud/components/molecules/KeymapItemSection.tsx b/src/cloud/components/molecules/KeymapItemSection.tsx
new file mode 100644
index 0000000000..c82ce6ff8d
--- /dev/null
+++ b/src/cloud/components/molecules/KeymapItemSection.tsx
@@ -0,0 +1,233 @@
+import React, {
+  KeyboardEventHandler,
+  useCallback,
+  useMemo,
+  useRef,
+  useState,
+} from 'react'
+import {
+  getGenericShortcutString,
+  KeymapItemEditableProps,
+} from '../../../lib/keymap'
+import Button from '../../../design/components/atoms/Button'
+import styled from '../../../design/lib/styled'
+import { inputStyle } from '../../../design/lib/styled/styleFunctions'
+import cc from 'classcat'
+import { useToast } from '../../../design/lib/stores/toast'
+import { useElectron } from '../../lib/stores/electron'
+
+const invalidShortcutInputs = [' ']
+const rejectedShortcutInputs = [' ', 'control', 'alt', 'shift', 'meta']
+
+interface KeymapItemSectionProps {
+  keymapKey: string
+  currentKeymapItem?: KeymapItemEditableProps
+  updateKeymap: (
+    key: string,
+    shortcutFirst: KeymapItemEditableProps,
+    shortcutSecond?: KeymapItemEditableProps
+  ) => Promise<void>
+  removeKeymap: (key: string) => void
+  description: string
+  desktopOnly: boolean
+}
+
+const KeymapItemSection = ({
+  keymapKey,
+  currentKeymapItem,
+  updateKeymap,
+  removeKeymap,
+  description,
+  desktopOnly,
+}: KeymapItemSectionProps) => {
+  const [inputError, setInputError] = useState<boolean>(false)
+  const [shortcutInputValue, setShortcutInputValue] = useState<string>('')
+  const [changingShortcut, setChangingShortcut] = useState<boolean>(false)
+  const [
+    currentShortcut,
+    setCurrentShortcut,
+  ] = useState<KeymapItemEditableProps | null>(
+    currentKeymapItem != null ? currentKeymapItem : null
+  )
+  const [
+    previousShortcut,
+    setPreviousShortcut,
+  ] = useState<KeymapItemEditableProps | null>(null)
+  const shortcutInputRef = useRef<HTMLInputElement>(null)
+
+  const { pushMessage } = useToast()
+  const { usingElectron } = useElectron()
+
+  const fetchInputShortcuts: KeyboardEventHandler<HTMLInputElement> = (
+    event
+  ) => {
+    event.stopPropagation()
+    event.preventDefault()
+    if (invalidShortcutInputs.includes(event.key.toLowerCase())) {
+      setInputError(true)
+      return
+    }
+
+    setInputError(false)
+
+    const shortcut: KeymapItemEditableProps = {
+      key: event.key.toUpperCase(),
+      keycode: event.keyCode,
+      modifiers: {
+        ctrl: event.ctrlKey,
+        alt: event.altKey,
+        shift: event.shiftKey,
+        meta: event.metaKey,
+      },
+    }
+    setCurrentShortcut(shortcut)
+    setShortcutInputValue(getGenericShortcutString(shortcut))
+  }
+
+  const applyKeymap = useCallback(() => {
+    if (currentShortcut == null) {
+      return
+    }
+    if (rejectedShortcutInputs.includes(currentShortcut.key.toLowerCase())) {
+      setInputError(true)
+      if (shortcutInputRef.current != null) {
+        shortcutInputRef.current.focus()
+      }
+      return
+    }
+
+    updateKeymap(keymapKey, currentShortcut, undefined)
+      .then(() => {
+        setChangingShortcut(false)
+        setInputError(false)
+      })
+      .catch(() => {
+        pushMessage({
+          title: 'Keymap assignment failed',
+          description: 'Cannot assign to already assigned shortcut',
+        })
+        setInputError(true)
+      })
+  }, [currentShortcut, keymapKey, updateKeymap, pushMessage])
+
+  const toggleChangingShortcut = useCallback(() => {
+    if (changingShortcut) {
+      applyKeymap()
+    } else {
+      setChangingShortcut(true)
+      setPreviousShortcut(currentShortcut)
+      if (currentShortcut != null) {
+        setShortcutInputValue(getGenericShortcutString(currentShortcut))
+      }
+    }
+  }, [applyKeymap, currentShortcut, changingShortcut])
+
+  const handleCancelKeymapChange = useCallback(() => {
+    setCurrentShortcut(previousShortcut)
+    setChangingShortcut(false)
+    setShortcutInputValue('')
+    setInputError(false)
+  }, [previousShortcut])
+
+  const handleRemoveKeymap = useCallback(() => {
+    setCurrentShortcut(null)
+    setPreviousShortcut(null)
+    setShortcutInputValue('')
+    removeKeymap(keymapKey)
+  }, [keymapKey, removeKeymap])
+
+  const shortcutString = useMemo(() => {
+    return currentShortcut != null && currentKeymapItem != null
+      ? getGenericShortcutString(currentKeymapItem)
+      : ''
+  }, [currentKeymapItem, currentShortcut])
+
+  const isEditableKeymap = !desktopOnly || (desktopOnly && usingElectron)
+
+  return (
+    <KeymapItemSectionContainer>
+      <div>{description}</div>
+      <KeymapItemInputSection>
+        {currentShortcut != null && currentKeymapItem != null && (
+          <ShortcutItemStyle>{shortcutString}</ShortcutItemStyle>
+        )}
+        {changingShortcut && (
+          <StyledInput
+            className={cc([inputError && 'error'])}
+            placeholder={'Press key'}
+            autoFocus={true}
+            ref={shortcutInputRef}
+            value={shortcutInputValue}
+            onChange={() => undefined}
+            onKeyDown={fetchInputShortcuts}
+          />
+        )}
+        {!isEditableKeymap ? (
+          <div>Desktop App Only</div>
+        ) : (
+          <Button variant={'primary'} onClick={toggleChangingShortcut}>
+            {currentShortcut == null
+              ? 'Assign'
+              : changingShortcut
+              ? 'Apply'
+              : 'Change'}
+          </Button>
+        )}
+        {isEditableKeymap && changingShortcut && (
+          <Button onClick={handleCancelKeymapChange}>Cancel</Button>
+        )}
+
+        {isEditableKeymap && currentShortcut != null && !changingShortcut && (
+          <Button onClick={handleRemoveKeymap}>Un-assign</Button>
+        )}
+      </KeymapItemInputSection>
+    </KeymapItemSectionContainer>
+  )
+}
+
+const ShortcutItemStyle = styled.div`
+  min-width: 88px;
+  max-width: 120px;
+  height: 32px;
+  font-size: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  background-color: ${({ theme }) => theme.colors.background.tertiary};
+  color: ${({ theme }) => theme.colors.text.primary};
+
+  border: 1px solid ${({ theme }) => theme.colors.border.main};
+  border-radius: 4px;
+`
+
+const StyledInput = styled.input`
+  ${inputStyle};
+  max-width: 120px;
+  min-width: 110px;
+  height: 1.3em;
+
+  &.error {
+    border: 1px solid red;
+  }
+`
+
+const KeymapItemSectionContainer = styled.div`
+  display: grid;
+  grid-template-columns: 45% minmax(55%, 400px);
+`
+
+const KeymapItemInputSection = styled.div`
+  display: grid;
+  grid-auto-flow: column;
+  align-items: center;
+
+  max-width: 380px;
+  justify-items: left;
+
+  margin-right: auto;
+
+  column-gap: 1em;
+`
+
+export default KeymapItemSection
diff --git a/src/cloud/components/settings/KeymapTab.tsx b/src/cloud/components/settings/KeymapTab.tsx
new file mode 100644
index 0000000000..6299e434de
--- /dev/null
+++ b/src/cloud/components/settings/KeymapTab.tsx
@@ -0,0 +1,106 @@
+import React, { useCallback, useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { getGenericShortcutString, KeymapItem } from '../../../lib/keymap'
+import Button from '../../../design/components/atoms/Button'
+import KeymapItemSection from '../molecules/KeymapItemSection'
+import styled from '../../../design/lib/styled'
+import { usePreferences } from '../../lib/stores/preferences'
+import Form from '../../../design/components/molecules/Form'
+import { lngKeys } from '../../lib/i18n/types'
+import FormRow from '../../../design/components/molecules/Form/templates/FormRow'
+import SettingTabContent from '../../../design/components/organisms/Settings/atoms/SettingTabContent'
+
+const KeymapTab = () => {
+  const {
+    preferences,
+    updateKeymap,
+    removeKeymap,
+    resetKeymap,
+  } = usePreferences()
+  const { t } = useTranslation()
+
+  const keymap = useMemo(() => {
+    const keymap = preferences['keymap']
+    return [...keymap.entries()]
+  }, [preferences])
+
+  const getKeymapItemSectionKey = useCallback((keymapItem: KeymapItem) => {
+    if (keymapItem.shortcutMainStroke == null) {
+      return keymapItem.description
+    } else {
+      return getGenericShortcutString(keymapItem.shortcutMainStroke)
+    }
+  }, [])
+
+  return (
+    <SettingTabContent
+      title={t(lngKeys.SettingsKeymap)}
+      body={
+        <Form
+          fullWidth={true}
+          rows={[
+            {
+              items: [
+                {
+                  type: 'node',
+                  element: (
+                    <KeymapItemList>
+                      {keymap != null &&
+                        keymap.map((keymapEntry: [string, KeymapItem]) => {
+                          return (
+                            <KeymapItemSection
+                              key={getKeymapItemSectionKey(keymapEntry[1])}
+                              keymapKey={keymapEntry[0]}
+                              currentKeymapItem={
+                                keymapEntry[1].shortcutMainStroke
+                              }
+                              description={keymapEntry[1].description}
+                              updateKeymap={updateKeymap}
+                              removeKeymap={removeKeymap}
+                              desktopOnly={
+                                keymapEntry[1].desktopOnly == null
+                                  ? false
+                                  : keymapEntry[1].desktopOnly
+                              }
+                            />
+                          )
+                        })}
+                    </KeymapItemList>
+                  ),
+                },
+              ],
+            },
+          ]}
+        >
+          <FormRow>
+            <KeymapHeaderSection>
+              <SectionResetKeymap>
+                <Button variant='danger' onClick={() => resetKeymap()}>
+                  Restore
+                </Button>
+              </SectionResetKeymap>
+            </KeymapHeaderSection>
+          </FormRow>
+        </Form>
+      }
+    />
+  )
+}
+
+const KeymapItemList = styled.div`
+  display: grid;
+  grid-template-rows: auto;
+  row-gap: 0.5em;
+`
+
+const KeymapHeaderSection = styled.div`
+  display: grid;
+  grid-template-columns: auto auto;
+`
+
+const SectionResetKeymap = styled.div`
+  margin-left: auto;
+  align-self: center;
+`
+
+export default KeymapTab
diff --git a/src/cloud/components/settings/SettingsComponent.tsx b/src/cloud/components/settings/SettingsComponent.tsx
index 52087fd5f6..cfdaad04b6 100644
--- a/src/cloud/components/settings/SettingsComponent.tsx
+++ b/src/cloud/components/settings/SettingsComponent.tsx
@@ -45,6 +45,7 @@ import AttachmentsTab from './AttachmentsTab'
 import ImportTab from './ImportTab'
 import TeamSubLimit from './TeamSubLimit'
 import { ExternalLink } from '../../../design/components/atoms/Link'
+import KeymapTab from './KeymapTab'
 
 const SettingsComponent = () => {
   const { t } = useTranslation()
@@ -107,6 +108,8 @@ const SettingsComponent = () => {
         return <PersonalInfoTab />
       case 'preferences':
         return <PreferencesTab />
+      case 'keymap':
+        return <KeymapTab />
       case 'teamInfo':
         return <TeamInfoTab />
       case 'teamMembers':
@@ -210,6 +213,12 @@ const SettingsComponent = () => {
             id='settings-personalInfoTab-btn'
             onClick={() => openSettingsTab('personalInfo')}
           />
+          <SettingNavButtonItem
+            label={t(lngKeys.SettingsKeymap)}
+            active={settingsTab === 'keymap'}
+            id='settings-keymap-btn'
+            onClick={() => openSettingsTab('keymap')}
+          />
           <SettingNavButtonItem
             label={t(lngKeys.SettingsPreferences)}
             active={settingsTab === 'preferences'}
diff --git a/src/cloud/lib/i18n/enUS.ts b/src/cloud/lib/i18n/enUS.ts
index cfcb4fce91..7f215f1a48 100644
--- a/src/cloud/lib/i18n/enUS.ts
+++ b/src/cloud/lib/i18n/enUS.ts
@@ -32,6 +32,7 @@ const enTranslation: TranslationSource = {
   [lngKeys.SettingsTeamInfo]: 'Settings',
   [lngKeys.SettingsTitle]: 'Settings',
   [lngKeys.SettingsPersonalInfo]: 'Settings',
+  [lngKeys.SettingsKeymap]: 'Keymap',
   [lngKeys.SettingsMarkdownPreview]: 'Markdown',
   [lngKeys.SettingsMarkdownPreviewShowcase]: 'Markdown Style Preview',
   [lngKeys.SettingsMarkdownPreviewCodeBlockTheme]: 'Code Block Theme',
diff --git a/src/cloud/lib/i18n/fr.ts b/src/cloud/lib/i18n/fr.ts
index d2f97792ea..8e27af7a2d 100644
--- a/src/cloud/lib/i18n/fr.ts
+++ b/src/cloud/lib/i18n/fr.ts
@@ -33,6 +33,7 @@ const frTranslation: TranslationSource = {
   [lngKeys.SettingsTeamInfo]: 'Paramètres',
   [lngKeys.SettingsTitle]: 'Paramètres',
   [lngKeys.SettingsPersonalInfo]: 'Paramètres',
+  [lngKeys.SettingsKeymap]: 'Carte clavier',
   [lngKeys.SettingsMarkdownPreview]: 'Markdown',
   [lngKeys.SettingsMarkdownPreviewShowcase]: 'Aperçu du style Markdown',
   [lngKeys.SettingsMarkdownPreviewCodeBlockTheme]: 'Thème du bloc de code',
diff --git a/src/cloud/lib/i18n/ja.ts b/src/cloud/lib/i18n/ja.ts
index fff22dad2d..82afc55b35 100644
--- a/src/cloud/lib/i18n/ja.ts
+++ b/src/cloud/lib/i18n/ja.ts
@@ -33,6 +33,7 @@ const jpTranslation: TranslationSource = {
   [lngKeys.SettingsTeamInfo]: '設定',
   [lngKeys.SettingsTitle]: '設定',
   [lngKeys.SettingsPersonalInfo]: '設定',
+  [lngKeys.SettingsKeymap]: 'キーマップ',
   [lngKeys.SettingsMarkdownPreview]: 'マークダウン',
   [lngKeys.SettingsMarkdownPreviewShowcase]: 'マークダウンスタイルのプレビュー',
   [lngKeys.SettingsMarkdownPreviewCodeBlockTheme]: 'コードブロックのテーマ',
diff --git a/src/cloud/lib/i18n/types.ts b/src/cloud/lib/i18n/types.ts
index 4031a91ca7..4e6d35b72c 100644
--- a/src/cloud/lib/i18n/types.ts
+++ b/src/cloud/lib/i18n/types.ts
@@ -108,6 +108,7 @@ export enum lngKeys {
   SettingsNotifications = 'settings.notifications',
   SettingsTitle = 'settings.title',
   SettingsPersonalInfo = 'settings.personalInfo',
+  SettingsKeymap = 'settings.keymap',
   SettingsMarkdownPreview = 'settings.markdownPreview',
   SettingsMarkdownPreviewShowcase = 'settings.markdownPreviewShowcase',
   SettingsMarkdownPreviewCodeBlockTheme = 'settings.markdownPreviewCodeBlockTheme',
diff --git a/src/cloud/lib/i18n/zhCN.ts b/src/cloud/lib/i18n/zhCN.ts
index ef4e8593bd..6a88056367 100644
--- a/src/cloud/lib/i18n/zhCN.ts
+++ b/src/cloud/lib/i18n/zhCN.ts
@@ -33,6 +33,7 @@ const zhTranslation: TranslationSource = {
   [lngKeys.SettingsTeamInfo]: '设置',
   [lngKeys.SettingsTitle]: '设置',
   [lngKeys.SettingsPersonalInfo]: '设置',
+  [lngKeys.SettingsKeymap]: '键盘映射',
   [lngKeys.SettingsPreferences]: '首选项',
   [lngKeys.SettingsMarkdownPreview]: '降价',
   [lngKeys.SettingsMarkdownPreviewShowcase]: 'Markdown 样式预览',
diff --git a/src/cloud/lib/shortcuts.ts b/src/cloud/lib/shortcuts.ts
index e4545adc58..057523b84f 100644
--- a/src/cloud/lib/shortcuts.ts
+++ b/src/cloud/lib/shortcuts.ts
@@ -32,13 +32,6 @@ export function isFocusLeftSideShortcut(event: KeyboardEvent) {
   )
 }
 
-export function isPageSearchShortcut(event: KeyboardEvent) {
-  return (
-    isWithGeneralCtrlKey(event) &&
-    event.key.toLowerCase() === shortcuts.pageSearch
-  )
-}
-
 export function isFocusRightSideShortcut(event: KeyboardEvent) {
   // cmd + shift + arrowRight
   return (
@@ -55,12 +48,6 @@ export function isShowHelpShortcut(event: KeyboardEvent) {
   )
 }
 
-export function isGlobalSearchShortcut(event: KeyboardEvent) {
-  return (
-    event.key.toLowerCase() === shortcuts.search && isWithGeneralCtrlKey(event)
-  )
-}
-
 /*** EDIT SESSION ***/
 export function isEditSessionSaveShortcut(event: KeyboardEvent) {
   return event.key.toLowerCase() === 'enter' && isWithGeneralCtrlKey(event)
@@ -109,7 +96,3 @@ export function isFolderDeleteShortcut(event: KeyboardEvent) {
 export function isFolderBookmarkShortcut(event: KeyboardEvent) {
   return isSingleKeyEventOutsideOfInput(event, shortcuts.bookmarkFolder)
 }
-
-export function isSidebarToggleShortcut(event: KeyboardEvent) {
-  return event.key === '0' && event.shiftKey && isWithGeneralCtrlKey(event)
-}
diff --git a/src/cloud/lib/stores/electron.ts b/src/cloud/lib/stores/electron.ts
index 628afb92b7..a313ea4c67 100644
--- a/src/cloud/lib/stores/electron.ts
+++ b/src/cloud/lib/stores/electron.ts
@@ -272,10 +272,6 @@ const useElectronStore = (): ElectronStore => {
         event.preventDefault()
         toggleSettingsEventEmitter.dispatch()
         return
-      case 'p':
-        event.preventDefault()
-        searchEventEmitter.dispatch()
-        return
       case '0':
         if (event.shiftKey) {
           event.preventDefault()
diff --git a/src/cloud/lib/stores/preferences/store.ts b/src/cloud/lib/stores/preferences/store.ts
index a9764b7602..b53551a260 100644
--- a/src/cloud/lib/stores/preferences/store.ts
+++ b/src/cloud/lib/stores/preferences/store.ts
@@ -8,10 +8,16 @@ import {
   cloudSidebaCategoryLabels,
   cloudSidebarOrderedCategoriesDelimiter,
 } from '../../sidebar'
-
-function savePreferencesToLocalStorage(preferences: Partial<Preferences>) {
-  localLiteStorage.setItem(preferencesKey, JSON.stringify(preferences))
-}
+import {
+  createCodemirrorTypeKeymap,
+  defaultKeymap,
+  findExistingShortcut,
+  getMenuAcceleratorForKeymapItem,
+  isMenuKeymap,
+  KeymapItem,
+  KeymapItemEditableProps,
+} from '../../../../lib/keymap'
+import { useElectron } from '../electron'
 
 const basePreferences: Preferences = {
   folderSortingOrder: 'Latest Updated',
@@ -30,13 +36,41 @@ const basePreferences: Preferences = {
   ),
   version: 1,
   docPropertiesAreHidden: false,
+  keymap: new Map<string, KeymapItem>(),
+}
+
+function replacer(_key: string, value: any) {
+  if (value instanceof Map && value.size > 0) {
+    return {
+      dataType: 'Map',
+      value: [...value.entries()],
+    }
+  } else {
+    return value
+  }
+}
+
+function reviver(_key: string, value: any) {
+  if (typeof value === 'object' && value !== null) {
+    if (value.dataType === 'Map') {
+      return new Map(value.value)
+    }
+  }
+  return value
+}
+
+function savePreferencesToLocalStorage(preferences: Partial<Preferences>) {
+  localLiteStorage.setItem(
+    preferencesKey,
+    JSON.stringify(preferences, replacer)
+  )
 }
 
 function getExistingPreferences() {
   try {
     const stringifiedPreferences = localLiteStorage.getItem(preferencesKey)
     if (stringifiedPreferences == null) return
-    const existingPreferences = JSON.parse(stringifiedPreferences)
+    const existingPreferences = JSON.parse(stringifiedPreferences, reviver)
     if (existingPreferences.version == null) {
       existingPreferences.version = 1
 
@@ -71,16 +105,25 @@ function usePreferencesStore() {
   const [preferences, setPreferences] = useSetState<Partial<Preferences>>(
     initialPreference
   )
+  const { sendToElectron, usingElectron } = useElectron()
   const hoverOffTimeoutRef = useRef<number>()
 
-  useEffect(() => {
-    savePreferencesToLocalStorage(preferences)
-  }, [preferences])
-
   const mergedPreferences = useMemo(() => {
+    const preferencesKeymap = preferences['keymap']
+    const keymap = basePreferences['keymap']
+    try {
+      if (preferencesKeymap != null) {
+        preferencesKeymap.forEach((value, key) => {
+          keymap.set(key, value)
+        })
+      }
+    } catch (e) {
+      console.warn('Corrupted storage, preferences keymap was null!')
+    }
     return {
       ...basePreferences,
       ...preferences,
+      keymap: keymap,
     }
   }, [preferences])
 
@@ -120,6 +163,174 @@ function usePreferencesStore() {
     setPreferences({ sidebarIsHovered: true })
   }, [setPreferences])
 
+  const keymap = mergedPreferences['keymap']
+  const getAcceleratorTypeKeymap = useCallback(
+    (key: string) => {
+      if (keymap == null) {
+        return ''
+      }
+      const keymapItem = keymap.get(key)
+      if (keymapItem == null) {
+        return ''
+      }
+      return getMenuAcceleratorForKeymapItem(keymapItem)
+    },
+    [keymap]
+  )
+
+  const getCodemirrorTypeKeymap = useCallback(
+    (key: string) => {
+      if (keymap == null) {
+        return null
+      }
+      const keymapItem = keymap.get(key)
+      if (keymapItem == null || keymapItem.shortcutMainStroke == null) {
+        return null
+      }
+      let keymapString = createCodemirrorTypeKeymap(
+        keymapItem.shortcutMainStroke
+      )
+      if (keymapItem.shortcutSecondStroke != null) {
+        keymapString +=
+          ' ' + createCodemirrorTypeKeymap(keymapItem.shortcutSecondStroke)
+      }
+      return keymapString
+    },
+    [keymap]
+  )
+
+  const updateKeymap = useCallback(
+    (
+      key: string,
+      firstShortcut: KeymapItemEditableProps,
+      secondShortcut?: KeymapItemEditableProps
+    ): Promise<void> => {
+      if (keymap == null) {
+        return Promise.reject('No keymap available')
+      }
+      if (findExistingShortcut(key, firstShortcut, keymap)) {
+        return Promise.reject('Existing keymap with the same shortcut')
+      }
+      const keymapItem = keymap.get(key)
+      if (keymapItem == null) {
+        return Promise.reject(`No such keymap to set for key: ${key}`)
+      }
+      keymap.set(key, {
+        description: keymapItem.description,
+        isMenuType: keymapItem.isMenuType,
+        shortcutMainStroke: {
+          ...keymapItem.shortcutMainStroke,
+          ...firstShortcut,
+        },
+        shortcutSecondStroke:
+          secondShortcut != null
+            ? {
+                ...keymapItem.shortcutSecondStroke,
+                ...secondShortcut,
+              }
+            : undefined,
+      })
+
+      setPreferences((preferences) => {
+        return {
+          ...preferences,
+          keymap: keymap,
+        }
+      })
+
+      if (isMenuKeymap(keymapItem)) {
+        if (usingElectron) {
+          sendToElectron('menuAcceleratorChanged', [
+            key,
+            getMenuAcceleratorForKeymapItem(keymapItem),
+          ])
+        }
+      }
+      return Promise.resolve()
+    },
+    [keymap, sendToElectron, setPreferences, usingElectron]
+  )
+
+  const removeKeymap = useCallback(
+    (key) => {
+      if (keymap == null) {
+        return false
+      }
+      const keymapItem = keymap.get(key)
+      if (keymapItem == null) {
+        return false
+      }
+      keymap.set(key, {
+        ...keymapItem,
+        shortcutMainStroke: undefined,
+        shortcutSecondStroke: undefined,
+      })
+      setPreferences((preferences) => {
+        return {
+          ...preferences,
+          keymap: keymap,
+        }
+      })
+
+      if (isMenuKeymap(keymapItem)) {
+        if (usingElectron) {
+          sendToElectron('menuAcceleratorChanged', [key, null])
+        }
+      }
+      return true
+    },
+    [keymap, sendToElectron, setPreferences, usingElectron]
+  )
+
+  const loadKeymaps = useCallback(() => {
+    const keymap = mergedPreferences['keymap']
+    for (const [key, keymapItem] of keymap) {
+      if (isMenuKeymap(keymapItem)) {
+        if (usingElectron) {
+          sendToElectron('menuAcceleratorChanged', [
+            key,
+            getMenuAcceleratorForKeymapItem(keymapItem),
+          ])
+        }
+      }
+    }
+
+    // add new keymaps to preferences if weren't available before
+    let addedKeymap = false
+    for (const [key, keymapItem] of defaultKeymap) {
+      if (!keymap.has(key)) {
+        keymap.set(key, keymapItem)
+        addedKeymap = true
+      }
+    }
+    if (addedKeymap) {
+      setPreferences((preferences) => {
+        return {
+          ...preferences,
+          keymap: keymap,
+        }
+      })
+    }
+  }, [mergedPreferences, sendToElectron, setPreferences, usingElectron])
+
+  const resetKeymap = useCallback(() => {
+    keymap.clear()
+    for (const [key, keymapItem] of defaultKeymap) {
+      keymap.set(key, keymapItem)
+    }
+    setPreferences((preferences) => {
+      return {
+        ...preferences,
+        keymap: defaultKeymap,
+      }
+    })
+  }, [keymap, setPreferences])
+
+  useEffect(() => {
+    loadKeymaps()
+    savePreferencesToLocalStorage(preferences)
+  }, [loadKeymaps, mergedPreferences, preferences, resetKeymap])
+
   return {
     preferences: mergedPreferences,
     setPreferences,
@@ -127,6 +338,11 @@ function usePreferencesStore() {
     hoverSidebarOff,
     hoverSidebarOn,
     topBarPaddingLeft,
+    updateKeymap,
+    removeKeymap,
+    resetKeymap,
+    getCodemirrorTypeKeymap,
+    getAcceleratorTypeKeymap,
   }
 }
 
diff --git a/src/cloud/lib/stores/preferences/types.ts b/src/cloud/lib/stores/preferences/types.ts
index 15d504b269..5465f6fb08 100644
--- a/src/cloud/lib/stores/preferences/types.ts
+++ b/src/cloud/lib/stores/preferences/types.ts
@@ -1,6 +1,7 @@
 import { SidebarState } from '../../../../design/lib/sidebar'
 import { SidebarTreeSortingOrder } from '../../../../design/lib/sidebar'
 import { DocStatus } from '../../../interfaces/db/doc'
+import { KeymapItem } from '../../../../lib/keymap'
 
 export type LayoutMode = 'split' | 'preview' | 'editor'
 
@@ -19,4 +20,5 @@ export interface Preferences {
   version?: number
   docStatusDisplayed: DocStatus[]
   docPropertiesAreHidden: boolean
+  keymap: Map<string, KeymapItem>
 }
diff --git a/src/cloud/lib/stores/settings/store.ts b/src/cloud/lib/stores/settings/store.ts
index 8e28b9abed..96253e3cc3 100644
--- a/src/cloud/lib/stores/settings/store.ts
+++ b/src/cloud/lib/stores/settings/store.ts
@@ -33,6 +33,7 @@ export const baseUserSettings: UserSettings = {
 export type SettingsTab =
   | 'personalInfo'
   | 'preferences'
+  | 'keymap'
   | 'teamInfo'
   | 'teamMembers'
   | 'integrations'
diff --git a/src/components/BoostHubWebview.tsx b/src/components/BoostHubWebview.tsx
index 337a49bf30..66ddaa3e31 100644
--- a/src/components/BoostHubWebview.tsx
+++ b/src/components/BoostHubWebview.tsx
@@ -360,6 +360,13 @@ const BoostHubWebview = ({
         case 'register-protocol':
           sendIpcMessage('register-protocol', [])
           break
+        case 'menuAcceleratorChanged':
+          if (event.args.length == 1) {
+            sendIpcMessage('menuAcceleratorChanged', event.args[0])
+          } else {
+            console.warn('Invalid arguments sent for menu accelerator change')
+          }
+          break
         default:
           console.log('Unhandled ipc message event', event.channel, event.args)
           break
diff --git a/src/components/atoms/BoostHubWebview.tsx b/src/components/atoms/BoostHubWebview.tsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/electron/index.ts b/src/electron/index.ts
index 2ce3a5f907..05c2062c02 100644
--- a/src/electron/index.ts
+++ b/src/electron/index.ts
@@ -18,6 +18,20 @@ const mac = process.platform === 'darwin'
 let ready = false
 
 const singleInstance = app.requestSingleInstanceLock()
+
+export const keymap = new Map<string, string>([
+  ['toggleGlobalSearch', 'CmdOrCtrl + p'],
+  ['toggleSplitEditMode', 'CmdOrCtrl + \\'],
+  ['togglePreviewMode', 'CmdOrCtrl + e'],
+  ['editorSaveAs', 'CmdOrCtrl + s'],
+  ['createNewDoc', 'CmdOrCtrl + n'],
+  // ['createNewFolder', 'CmdOrCtrl + Shift + N'],
+  ['resetZoom', 'CmdOrCtrl + 0'],
+  ['zoomOut', 'CmdOrCtrl + -'],
+  ['zoomIn', 'CmdOrCtrl + Plus'],
+  ['openPreferences', 'CmdOrCtrl + ,'],
+])
+
 if (!singleInstance) {
   app.quit()
 } else {
@@ -82,7 +96,7 @@ app.on('ready', () => {
     )}`
   )
 
-  applyMenuTemplate(getTemplateFromKeymap())
+  applyMenuTemplate(getTemplateFromKeymap(keymap))
 
   // multiple windows support
   ipcMain.on('new-window-event', (args: any) => {
@@ -121,4 +135,15 @@ app.on('ready', () => {
       window.webContents.send('open-boostnote-url', url)
     })
   })
+
+  ipcMain.on('menuAcceleratorChanged', (_, args) => {
+    if (args.length != 2) {
+      return
+    }
+    const menuItemId = args[0]
+    const newAcceleratorShortcut = args[1] == null ? undefined : args[1]
+
+    keymap.set(menuItemId, newAcceleratorShortcut)
+    applyMenuTemplate(getTemplateFromKeymap(keymap))
+  })
 })
diff --git a/src/electron/menu.ts b/src/electron/menu.ts
index 574ed9ab3e..a945799bf4 100644
--- a/src/electron/menu.ts
+++ b/src/electron/menu.ts
@@ -12,16 +12,18 @@ import { electronFrontendUrl } from './consts'
 
 const mac = process.platform === 'darwin'
 
-export function getTemplateFromKeymap(): MenuItemConstructorOptions[] {
+export function getTemplateFromKeymap(
+  keymap: Map<string, string>
+): MenuItemConstructorOptions[] {
   const menu: MenuItemConstructorOptions[] = []
 
   if (mac) {
     menu.push(getMacRootMenu())
   }
 
-  menu.push(getFileMenu())
-  menu.push(getEditMenu())
-  menu.push(getViewMenu())
+  menu.push(getFileMenu(keymap))
+  menu.push(getEditMenu(keymap))
+  menu.push(getViewMenu(keymap))
   menu.push(getWindowMenu())
   menu.push(getCommunityMenu())
   menu.push({
@@ -73,7 +75,7 @@ function getMacRootMenu(): MenuItemConstructorOptions {
   }
 }
 
-function getFileMenu(): MenuItemConstructorOptions {
+function getFileMenu(keymap: Map<string, string>): MenuItemConstructorOptions {
   const submenuItems: MenuItemConstructorOptions[] = mac
     ? [
         {
@@ -89,14 +91,14 @@ function getFileMenu(): MenuItemConstructorOptions {
           type: 'normal',
           label: 'New Document',
           click: createEmitIpcMenuItemHandler('new-doc'),
-          accelerator: 'Cmd + N',
+          accelerator: keymap.get('createNewDoc'),
         },
         { type: 'separator' },
         {
           type: 'normal',
           label: 'Save As',
           click: createEmitIpcMenuItemHandler('save-as'),
-          accelerator: 'Cmd + S',
+          accelerator: keymap.get('editorSaveAs'),
         },
         { type: 'separator' },
         { role: 'close' },
@@ -115,14 +117,14 @@ function getFileMenu(): MenuItemConstructorOptions {
           type: 'normal',
           label: 'New Document',
           click: createEmitIpcMenuItemHandler('new-doc'),
-          accelerator: 'Ctrl + N',
+          accelerator: keymap.get('createNewDoc'),
         },
         { type: 'separator' },
         {
           type: 'normal',
           label: 'Save As',
           click: createEmitIpcMenuItemHandler('save-as'),
-          accelerator: 'Ctrl + S',
+          accelerator: keymap.get('editorSaveAs'),
         },
         { type: 'separator' },
         {
@@ -139,7 +141,7 @@ function getFileMenu(): MenuItemConstructorOptions {
         { type: 'separator' },
         {
           label: 'Preferences',
-          accelerator: 'Ctrl+,',
+          accelerator: keymap.get('openPreferences'),
           click: createEmitIpcMenuItemHandler('toggle-settings'),
         },
         { type: 'separator' },
@@ -152,7 +154,7 @@ function getFileMenu(): MenuItemConstructorOptions {
   }
 }
 
-function getEditMenu(): MenuItemConstructorOptions {
+function getEditMenu(keymap: Map<string, string>): MenuItemConstructorOptions {
   const submenuItems: MenuItemConstructorOptions[] = [
     { role: 'undo' },
     { role: 'redo' },
@@ -165,7 +167,7 @@ function getEditMenu(): MenuItemConstructorOptions {
       type: 'normal',
       label: 'Search',
       click: createEmitIpcMenuItemHandler('search'),
-      accelerator: mac ? 'Cmd + P' : 'Ctrl + P',
+      accelerator: keymap.get('toggleGlobalSearch'),
     },
     { type: 'separator' },
     { role: 'delete' },
@@ -188,7 +190,7 @@ function getEditMenu(): MenuItemConstructorOptions {
   }
 }
 
-function getViewMenu(): MenuItemConstructorOptions {
+function getViewMenu(keymap: Map<string, string>): MenuItemConstructorOptions {
   const submenuItems: MenuItemConstructorOptions[] = [
     {
       type: 'submenu',
@@ -315,9 +317,9 @@ function getViewMenu(): MenuItemConstructorOptions {
     },
 
     { type: 'separator' },
-    { role: 'resetZoom' },
-    { role: 'zoomIn' },
-    { role: 'zoomOut' },
+    { role: 'resetZoom', accelerator: keymap.get('resetZoom') },
+    { role: 'zoomIn', accelerator: keymap.get('zoomIn') },
+    { role: 'zoomOut', accelerator: keymap.get('zoomOut') },
     { type: 'separator' },
     { role: 'togglefullscreen' },
   ]
diff --git a/src/electron/windows.ts b/src/electron/windows.ts
index 28ea042bc5..aafd130efb 100644
--- a/src/electron/windows.ts
+++ b/src/electron/windows.ts
@@ -9,6 +9,7 @@ import {
 import path from 'path'
 import { dev } from './consts'
 import { getTemplateFromKeymap } from './menu'
+import { keymap } from './index'
 
 const windows = new Set<BrowserWindow>()
 
@@ -54,7 +55,7 @@ export function createAWindow(
     window.show()
   })
 
-  applyMenuTemplate(getTemplateFromKeymap())
+  applyMenuTemplate(getTemplateFromKeymap(keymap))
 
   if (MAC) {
     window.on('close', (event) => {
diff --git a/src/lib/keymap.ts b/src/lib/keymap.ts
new file mode 100644
index 0000000000..a9ebbd6441
--- /dev/null
+++ b/src/lib/keymap.ts
@@ -0,0 +1,487 @@
+import { osName } from '../design/lib/platform'
+
+interface ModifierItem {
+  ctrl?: boolean
+  shift?: boolean
+  alt?: boolean
+  meta?: boolean
+}
+
+export interface KeymapItemEditableProps {
+  key: string
+  keycode: number
+  modifiers?: ModifierItem
+}
+
+export interface KeymapItem {
+  shortcutMainStroke?: KeymapItemEditableProps
+  shortcutSecondStroke?: KeymapItemEditableProps
+  description: string
+  isMenuType?: boolean
+  desktopOnly?: boolean
+}
+
+export const defaultKeymap = new Map<string, KeymapItem>([
+  [
+    'createNewDoc',
+    {
+      shortcutMainStroke: {
+        key: 'n',
+        keycode: 78,
+        modifiers:
+          osName === 'macos'
+            ? { meta: true }
+            : {
+                ctrl: true,
+              },
+      },
+      description: 'Create new document',
+      isMenuType: true,
+      desktopOnly: true,
+    },
+  ],
+  // todo: [komediruzecki-2021-12-5] Enable when available
+  // [
+  //   'createNewFolder',
+  //   {
+  //     shortcutMainStroke: {
+  //       key: 'N',
+  //       keycode: 78,
+  //       modifiers:
+  //         osName === 'macos'
+  //           ? { meta: true, shift: true }
+  //           : {
+  //               ctrl: true,
+  //               shift: true,
+  //             },
+  //     },
+  //     description: 'Create new folder',
+  //     isMenuType: true,
+  //     desktopOnly: true,
+  //   },
+  // ],
+  [
+    'toggleSideNav',
+    {
+      shortcutMainStroke: {
+        key: '0',
+        keycode: 48,
+        modifiers:
+          osName === 'macos'
+            ? { meta: true, shift: true }
+            : {
+                ctrl: true,
+                shift: true,
+              },
+      },
+      description: 'Toggle side nav',
+    },
+  ],
+  [
+    'toggleGlobalSearch',
+    {
+      shortcutMainStroke: {
+        key: 'p',
+        keycode: 80,
+        modifiers:
+          osName === 'macos'
+            ? { meta: true }
+            : {
+                ctrl: true,
+              },
+      },
+      description: 'Toggle global search modal dialog',
+      isMenuType: true,
+    },
+  ],
+  [
+    'toggleInPageSearch',
+    {
+      shortcutMainStroke: {
+        key: 'F',
+        keycode: 70,
+        modifiers:
+          osName === 'macos'
+            ? { meta: true, shift: true }
+            : {
+                ctrl: true,
+                shift: true,
+              },
+      },
+      description: 'Toggle in-page search modal dialog',
+      desktopOnly: true,
+    },
+  ],
+  // todo: [komediruzecki-2021-11-7] enable once implemented
+  // [
+  //   'toggleLocalSearch',
+  //   {
+  //     shortcutMainStroke: {
+  //       key: 'F',
+  //       keycode: 70,
+  //       modifiers:
+  //         osName === 'macos'
+  //           ? { meta: true }
+  //           : {
+  //               ctrl: true,
+  //             },
+  //     },
+  //     description: 'Toggle local editor search modal dialog',
+  //   },
+  // ],
+  // [
+  //   'toggleLocalReplace',
+  //   {
+  //     shortcutMainStroke: {
+  //       key: 'F',
+  //       keycode: 70,
+  //       modifiers:
+  //         osName === 'macos'
+  //           ? { meta: true, alt: true }
+  //           : {
+  //               ctrl: true,
+  //               alt: true,
+  //             },
+  //     },
+  //     description: 'Toggle local editor replace modal dialog',
+  //   },
+  // ],
+  // todo: [komediruzecki-2021-11-7] enable once implemented
+  // [
+  //   'editorSaveAs',
+  //   {
+  //     shortcutMainStroke: {
+  //       key: 'S',
+  //       keycode: 83,
+  //       modifiers:
+  //         osName === 'macos'
+  //           ? { meta: true }
+  //           : {
+  //               ctrl: true,
+  //             },
+  //     },
+  //     description: 'Export open document (save as)',
+  //     isMenuType: true,
+  //   },
+  // ],
+  [
+    'togglePreviewMode',
+    {
+      shortcutMainStroke: {
+        key: 'e',
+        keycode: 69,
+        modifiers:
+          osName === 'macos'
+            ? { meta: true }
+            : {
+                ctrl: true,
+              },
+      },
+      description: 'Toggle preview mode in editor',
+      isMenuType: true,
+    },
+  ],
+  [
+    'toggleSplitEditMode',
+    {
+      shortcutMainStroke: {
+        key: '\\',
+        keycode: 220,
+        modifiers:
+          osName === 'macos'
+            ? { meta: true }
+            : {
+                ctrl: true,
+              },
+      },
+      description: 'Toggles split edit mode in editor',
+      isMenuType: true,
+    },
+  ],
+  [
+    'zoomIn',
+    {
+      shortcutMainStroke: {
+        key: '+',
+        keycode: 61,
+        modifiers:
+          osName === 'macos'
+            ? { meta: true }
+            : {
+                ctrl: true,
+              },
+      },
+      description: 'Zoom in window',
+      isMenuType: true,
+      desktopOnly: true,
+    },
+  ],
+  [
+    'zoomOut',
+    {
+      shortcutMainStroke: {
+        key: '-',
+        keycode: 173,
+        modifiers:
+          osName === 'macos'
+            ? { meta: true }
+            : {
+                ctrl: true,
+              },
+      },
+      description: 'Zoom out window',
+      isMenuType: true,
+      desktopOnly: true,
+    },
+  ],
+  [
+    'resetZoom',
+    {
+      shortcutMainStroke: {
+        key: '0',
+        keycode: 48,
+        modifiers:
+          osName === 'macos'
+            ? { meta: true }
+            : {
+                ctrl: true,
+              },
+      },
+      description: 'Reset window zoom',
+      isMenuType: true,
+      desktopOnly: true,
+    },
+  ],
+  // todo: [komediruzecki-2021-11-7] Enable once implemented
+  // [
+  //   'focusEditor',
+  //   {
+  //     shortcutMainStroke: {
+  //       key: 'J',
+  //       keycode: 74,
+  //       modifiers:
+  //         osName === 'macos'
+  //           ? { meta: true }
+  //           : {
+  //               ctrl: true,
+  //             },
+  //     },
+  //     description: 'Focus document editor',
+  //     isMenuType: true,
+  //   },
+  // ],
+  // [
+  //   'focusDocTitle',
+  //   {
+  //     shortcutMainStroke: {
+  //       key: 'J',
+  //       keycode: 74,
+  //       modifiers:
+  //         osName === 'macos'
+  //           ? { meta: true, shift: true }
+  //           : {
+  //               ctrl: true,
+  //               shift: true,
+  //             },
+  //     },
+  //     description: 'Focus document title',
+  //     isMenuType: true,
+  //   },
+  // ],
+  [
+    'openPreferences',
+    {
+      shortcutMainStroke: {
+        key: ',',
+        keycode: 188,
+        modifiers:
+          osName === 'macos'
+            ? { meta: true }
+            : {
+                ctrl: true,
+              },
+      },
+      description: 'Open Preferences',
+      isMenuType: true,
+    },
+  ],
+  // todo: [komediruzecki-2021-11-7] Enable when feature implemented
+  // initially un-assigned list
+  // [
+  //   'insertLocaleDateString',
+  //   {
+  //     description: 'Insert locale date',
+  //     isMenuType: false,
+  //   },
+  // ],
+  // [
+  //   'insertDateAndTimeString',
+  //   {
+  //     description: 'Insert date and time',
+  //     isMenuType: false,
+  //   },
+  // ],
+])
+
+export function compareEventKeyWithKeymap(
+  keymapProps: KeymapItem | undefined,
+  event: KeyboardEvent
+) {
+  if (keymapProps == null || keymapProps.shortcutMainStroke == null) {
+    return
+  }
+  const eventProps: KeymapItemEditableProps = {
+    key: event.key.toUpperCase(),
+    modifiers: {
+      ctrl: event.ctrlKey,
+      shift: event.shiftKey,
+      alt: event.altKey,
+      meta: event.metaKey,
+    },
+    keycode: event.keyCode,
+  }
+  return areShortcutsEqual(keymapProps.shortcutMainStroke, eventProps)
+}
+
+export function createCodemirrorTypeKeymap(
+  keymapProps: KeymapItemEditableProps
+) {
+  let keymapString = ''
+  if (keymapProps.modifiers != null) {
+    if (keymapProps.modifiers.shift != null) {
+      keymapString += keymapProps.modifiers.shift ? 'Shift-' : ''
+    }
+    if (keymapProps.modifiers.meta != null) {
+      keymapString += keymapProps.modifiers.meta ? 'Cmd-' : ''
+    }
+    if (keymapProps.modifiers.ctrl != null) {
+      keymapString += keymapProps.modifiers.ctrl ? 'Ctrl-' : ''
+    }
+    if (keymapProps.modifiers.alt != null) {
+      keymapString += keymapProps.modifiers.alt ? 'Alt-' : ''
+    }
+  }
+
+  const keyLowercase = keymapProps.key.toLowerCase()
+  if (
+    keyLowercase != 'control' &&
+    keyLowercase != 'shift' &&
+    keyLowercase != 'alt' &&
+    keyLowercase != 'meta'
+  ) {
+    keymapString += keymapProps.key
+  }
+  return keymapString
+}
+
+function convertNullToFalse(value?: boolean) {
+  return value == null ? false : value
+}
+
+function areShortcutsEqual(
+  first: KeymapItemEditableProps,
+  second: KeymapItemEditableProps
+) {
+  if (first.keycode != second.keycode || first.key != second.key) {
+    return false
+  }
+  if (first.modifiers && second.modifiers) {
+    if (
+      convertNullToFalse(first.modifiers.ctrl) !=
+      convertNullToFalse(second.modifiers.ctrl)
+    ) {
+      return false
+    }
+
+    if (
+      convertNullToFalse(first.modifiers.shift) !=
+      convertNullToFalse(second.modifiers.shift)
+    ) {
+      return false
+    }
+    if (
+      convertNullToFalse(first.modifiers.alt) !=
+      convertNullToFalse(second.modifiers.alt)
+    ) {
+      return false
+    }
+  } else {
+    if (
+      (first.modifiers == null && second.modifiers != null) ||
+      (first.modifiers != null && second.modifiers == null)
+    ) {
+      return false
+    }
+  }
+
+  return true
+}
+
+export function findExistingShortcut(
+  shortcutKey: string,
+  shortcut: KeymapItemEditableProps,
+  keymap: Map<string, KeymapItem>
+) {
+  for (const [key, keymapShortcut] of keymap) {
+    if (key == shortcutKey) {
+      continue
+    }
+    if (keymapShortcut.shortcutMainStroke != null) {
+      if (areShortcutsEqual(shortcut, keymapShortcut.shortcutMainStroke)) {
+        return true
+      }
+    }
+  }
+  return false
+}
+
+export function isMenuKeymap(keymapItem: KeymapItem): boolean {
+  return convertNullToFalse(keymapItem.isMenuType)
+}
+
+export function getGenericShortcutString(
+  shortcut: KeymapItemEditableProps
+): string {
+  return createCodemirrorTypeKeymap(shortcut)
+}
+
+export function getMenuAcceleratorKeymapKey(key: string): string {
+  switch (key) {
+    default:
+      return key
+    case '+':
+      return 'Plus'
+    case ' ':
+      return 'Space'
+    case '\t':
+      return 'Tab'
+  }
+}
+
+export function getMenuAcceleratorForKeymapItem(
+  keymapItem: KeymapItem
+): string {
+  const keymapProps = keymapItem.shortcutMainStroke
+  if (keymapProps == null) {
+    return ''
+  }
+  let keymapString = ''
+  if (keymapProps.modifiers != null) {
+    if (keymapProps.modifiers.ctrl != null) {
+      keymapString += keymapProps.modifiers.ctrl ? 'Ctrl + ' : ''
+    }
+    if (keymapProps.modifiers.shift != null) {
+      keymapString += keymapProps.modifiers.shift ? 'Shift + ' : ''
+    }
+    if (keymapProps.modifiers.alt != null) {
+      keymapString += keymapProps.modifiers.alt ? 'Alt + ' : ''
+    }
+    if (keymapProps.modifiers.meta != null) {
+      keymapString += keymapProps.modifiers.meta ? 'Meta + ' : ''
+    }
+  }
+
+  const menuKeymapKey = getMenuAcceleratorKeymapKey(keymapProps.key)
+  keymapString += menuKeymapKey
+  return keymapString
+}