[
  {
    "path": ".gitignore",
    "content": "pom.xml\n*jar\n/lib/\n/classes/\n/out/\n/target/\n.lein-deps-sum\n.lein-repl-history\n.lein-plugins/\n/script/\n"
  },
  {
    "path": "CHANGES.md",
    "content": "## 0.1.8\n### Changes\n* Stop depending on _rootNodeId so that we can support React 0.13.\n\n## 0.1.7\n### Changes\n* Fix ordering of render-ms and mount-ms\n\n## 0.1.6\n### Changes\n* fix borked release to Clojars of 0.1.5\n\n## 0.1.5\n### Changes\n* Note: this released was messed up somehow, use 0.1.6 instead\n* fix un-required goog.string.format. Thanks [@cldwalker](https://github.com/cldwalker)!\n\n## 0.1.4\n### Changes\n* fixes compilation error from requiring `goog`\n\n## 0.1.3\n### Changes\n* remove unused cljs-time requires\n"
  },
  {
    "path": "README.md",
    "content": "# Om-instrumentation\n\nInstrumentation helper for Om applications.\n\n[![Circle CI](https://circleci.com/gh/PrecursorApp/om-i.svg?style=svg)](https://circleci.com/gh/PrecursorApp/om-i)\n\n## Overview\n\nOm-i (pronounced \"Oh, my!\") helps you identify the components in your [Om](https://github.com/omcljs/om) application that are being passed too much of your app state and rendering unnecessarily. It provides useful statistics about render times and frequencies for all of your components.\n\nYou can see it live on [Precursor](https://precursorapp.com), a collaborative drawing application. Use Ctrl+Alt+Shift+j to toggle it.\n\n<a href=\"https://precursorapp.com\">\n <img src=\"http://dtwdl3ecuoduc.cloudfront.net/om-i/instrumentation.gif\" />\n</a>\n\n## Setup\n\n### Dependencies\nAdd Om-i to your project's dependencies\n\n```\n[precursor/om-i \"0.1.8\"]\n```\n\n### Enable the instrumentation\n\nUse Om-i's custom descriptor so that it can gather render times for your components. To enable it globally, use the `:instrument` opt in `om/root`\n\n```clojure\n(om/root\n  app-component\n  app-state\n  {:target container\n   :instrument (fn [f cursor m]\n                 (om/build* f cursor\n                            (assoc m\n                                   :descriptor om-i.core/instrumentation-methods)))})\n```\n\n### Mount the component\n\nAdd the following somewhere in your setup code. If you're using figwheel, place it somewhere that won't get reloaded.\n\n```clojure\n(om-i.core/setup-component-stats!)\n```\n\nOm-i renders its statistics in a separate root so that it doesn't interact with your application.\n\nIt will create a `div` in the body with classname \"om-instrumentation\" by default and assign three keyboard shortcuts: Ctrl+Alt+Shift+j to bring down the statistics menu, Ctrl+Alt+Shift+k to clear the statistics, and Ctrl+Alt+Shift+s to switch the sort order.\n\n\nYou can override the defaults with:\n\n```clojure\n(om-i.core/setup-component-stats! {:class           \"om-instrumentation\"\n                                   :clear-shortcut  #{\"ctrl\" \"alt\" \"shift\" \"k\"}\n                                   :toggle-shortcut #{\"ctrl\" \"alt\" \"shift\" \"j\"}\n                                   :sort-shorcut    #{\"ctrl\" \"alt\" \"shift\" \"s\"}})\n```\n\n### Styles\n\nYou need to set up css styles to handle displaying the instrumentation when it's opened. There are sample less and css files in the resources directory.\n\nIf you want to try out Om-i, or just use it in development, we've provided a helper that will embed a style tag with the syles from resources/om-i.min.css.\n\n```clojure\n(om-i.hacks/insert-styles)\n```\n\nIt's not recommended to use this in production.\n\n### Wrapping a pre-existing descriptor\n\nIf you're already using a custom descriptor, you can still use Om-i. Here's an example wrapping Om's `no-local-descriptor`.\n\n```clojure\n(let [methods (om-i.core/instrument-methods om/no-local-state-methods)\n      descriptor (om/no-local-descriptor methods)]\n  (om/root\n    app-component\n    app-state\n    {:target container\n     :instrument (fn [f cursor m]\n                   (om/build* f cursor (assoc m :descriptor descriptor)))}))\n```\n\n## Acknowledgements\n\nThanks to [@sgrove](https://github.com/sgrove) for his keyboard handling code. Om-i uses a minimal version of the code he wrote for Precursor. There is an older, [public version of the code in Omchaya](https://github.com/sgrove/omchaya/blob/master/src/omchaya/components/key_queue.cljs).\n\nThanks to [@brandonbloom](https://github.com/brandonbloom) for demonstrating how to use descriptors in Om. [Related blog post](http://blog.circleci.com/local-state-global-concerns/).\n\nThanks to [@swannodette](https://github.com/swannodette) for releasing Om.\n\n\n## License\n\nCopyright © 2015 PrecursorApp\n\nDistributed under the Eclipse Public License either version 1.0 or (at your option) any later version.\n"
  },
  {
    "path": "circle.yml",
    "content": "dependencies:\n  post:\n    - lein cljsbuild once\n\ntest:\n  override:\n    - echo no tests\n"
  },
  {
    "path": "project.clj",
    "content": "(defproject precursor/om-i \"0.1.8\"\n  :description \"Instrumentation helpers for Om applications\"\n  :url \"https://github.com/PrecursorApp/om-i\"\n  :license {:name \"Eclipse Public License\"\n            :url \"http://www.eclipse.org/legal/epl-v10.html\"}\n  :dependencies [[org.clojure/clojure \"1.6.0\"]\n                 [org.clojure/clojurescript \"0.0-2755\" :scope \"provided\"]\n                 [org.omcljs/om \"0.8.8\" :scope \"provided\"]]\n\n  :plugins [[lein-cljsbuild \"1.0.4\"]]\n  :cljsbuild {:builds [{:id \"test\"\n                        :source-paths [\"src\"]\n                        :compiler {:output-to \"script/tests.simple.js\"\n                                   :output-dir \"script/out\"\n                                   :source-map \"script/tests.simple.js.map\"\n                                   :output-wrapper false\n                                   :optimizations :simple}}]}\n\n  :source-paths [\"src\"])\n"
  },
  {
    "path": "resources/om-i.css",
    "content": "@keyframes in-fade-top-soft {\n  0% {\n    opacity: 0;\n    transform: translate3d(0, -4rem, 0);\n  }\n  100% {\n    opacity: 1;\n    transform: none;\n  }\n}\n@-webkit-keyframes in-fade-top-soft {\n  0% {\n    opacity: 0;\n    -webkit-transform: translate3d(0, -4rem, 0);\n  }\n  100% {\n    opacity: 1;\n    -webkit-transform: none;\n  }\n}\n.om-instrumentation {\n  user-select: none;\n  -moz-user-select: none;\n  -webkit-user-select: none;\n  pointer-events: none;\n  color: #888888;\n  position: fixed;\n  z-index: 1000;\n  top: 0;\n  left: 0;\n  width: 100%;\n}\n.instrumentation-table {\n  -webkit-animation: in-fade-top-soft 500ms;\n  animation: in-fade-top-soft 500ms;\n  background-color: rgba(0, 0, 0, 0.6);\n  font-family: Monaco, monospace;\n  font-size: .75rem;\n  line-height: 2;\n  width: 100%;\n}\n.instrumentation-table th {\n  color: #ffffff;\n  line-height: 1rem;\n  padding: 1.5rem 0;\n  text-transform: uppercase;\n  text-align: left;\n}\n.instrumentation-table th:not(:first-child) {\n  text-align: center;\n}\n.instrumentation-table th.left {\n  text-align: left;\n}\n.instrumentation-table th.right {\n  text-align: right;\n}\n.instrumentation-table th,\n.instrumentation-table td {\n  white-space: pre-wrap;\n}\n.instrumentation-table th:first-child,\n.instrumentation-table td:first-child {\n  padding-left: 1.5rem;\n}\n.instrumentation-table tbody tr:nth-child(odd) {\n  background-color: rgba(0, 0, 0, 0.4);\n}\n.instrumentation-table tbody td:nth-child(even) {\n  text-align: right;\n  border-right: 1px dashed;\n  padding-right: .5em;\n}\n.instrumentation-table tbody td:nth-child(odd) {\n  text-align: left;\n  padding-left: .5em;\n}\n.instrumentation-table tbody td:first-child {\n  padding-left: 1.5rem;\n}\n.instrumentation-table tfoot td {\n  line-height: 1rem;\n  text-align: center;\n  padding: 1.5rem 0;\n}\n.instrumentation-table small {\n  font-size: 1em;\n  opacity: .5;\n}\n"
  },
  {
    "path": "resources/om-i.less",
    "content": "@black: #000;\n@white: #fff;\n@gray:  #888;\n\n@font_mono: Monaco, monospace;\n@zindex-instrumentation: 1000;\n\n@tile: (1rem * 4);\n@menu_padding: 1.5rem;\n\n@keyframes in-fade-top-soft {\n  0%   { opacity: 0; transform: translate3d(0, -@tile, 0);}\n  100% { opacity: 1; transform: none;}\n}\n@-webkit-keyframes in-fade-top-soft {\n  0%   { opacity: 0; -webkit-transform: translate3d(0, -@tile, 0);}\n  100% { opacity: 1; -webkit-transform: none;}\n}\n.om-instrumentation {\n  user-select: none;\n  -moz-user-select: none;\n  -webkit-user-select: none;\n  pointer-events: none;\n  color: @gray;\n  position: fixed;\n  z-index: @zindex-instrumentation;\n  top: 0;\n  left: 0;\n  width: 100%;\n}\n.instrumentation-table {\n  -webkit-animation: in-fade-top-soft 500ms;\n  animation: in-fade-top-soft 500ms;\n  background-color: fade(@black, 60);\n  font-family: @font_mono;\n  font-size: .75rem;\n  line-height: 2;\n  width: 100%;\n  th {\n    color: @white;\n    line-height: 1rem;\n    padding: @menu_padding 0;\n    text-transform: uppercase;\n    text-align: left;\n    &:not(:first-child) {\n      text-align: center;\n    }\n    &.left {\n      text-align: left;\n    }\n    &.right {\n      text-align: right;\n    }\n  }\n  th,\n  td {\n    white-space: pre-wrap;\n    &:first-child {\n      padding-left: @menu_padding;\n    }\n  }\n  tbody {\n    tr {\n      &:nth-child(odd) {\n        background-color: fade(@black, 40);\n      }\n    }\n    td {\n      &:nth-child(even) {\n        text-align: right;\n        border-right: 1px dashed;\n        padding-right: .5em;\n      }\n      &:nth-child(odd) {\n        text-align: left;\n        padding-left: .5em;\n      }\n      &:first-child {\n        padding-left: @menu_padding;\n      }\n    }\n  }\n  tfoot {\n    td {\n      line-height: 1rem;\n      text-align: center;\n      padding: @menu_padding 0;\n    }\n  }\n  small {\n    font-size: 1em;\n    opacity: .5;\n  }\n}\n"
  },
  {
    "path": "src/om_i/core.cljs",
    "content": "(ns om-i.core\n  (:require [clojure.string :as str]\n            [goog.dom]\n            [goog.string :as gstring]\n            [goog.string.format]\n            [om.core :as om :include-macros true]\n            [om.dom :as dom :include-macros true]\n            [om-i.keyboard :as keyboard]))\n\n;; map of display name to component render stats, e.g.\n;; {\"App\" {:last-will-update <time 3pm> :display-name \"App\" :last-did-update <time 3pm> :render-ms [10 39 20 40]}}\n(defonce component-stats (atom {}))\n\n(defn wrap-will-update\n  \"Tracks last call time of componentWillUpdate for each component, then calls\n   the original componentWillUpdate.\"\n  [f]\n  (fn [next-props next-state]\n    (this-as this\n      (swap! component-stats update-in [((aget this \"getDisplayName\"))]\n             merge {:display-name ((aget this \"getDisplayName\"))\n                    :last-will-update (goog/now)})\n      (.call f this next-props next-state))))\n\n(defn wrap-did-update\n  \"Tracks last call time of componentDidUpdate for each component and updates\n   the render times (using start time provided by wrap-will-update), then\n   calls the original componentDidUpdate.\"\n  [f]\n  (fn [prev-props prev-state]\n    (this-as this\n      (swap! component-stats update-in [((aget this \"getDisplayName\"))]\n             (fn [stats]\n               (let [now (goog/now)]\n                 (-> stats\n                   (assoc :last-did-update now)\n                   (update-in [:render-ms] (fnil conj [])\n                              (max (- now (:last-will-update stats now)) 0))))))\n      (.call f this prev-props prev-state))))\n\n(defn wrap-will-mount\n  \"Tracks last call time of componentWillMount for each component, then calls\n   the original componentWillMount.\"\n  [f]\n  (fn []\n    (this-as this\n      (swap! component-stats update-in [((aget this \"getDisplayName\"))]\n             merge {:display-name ((aget this \"getDisplayName\"))\n                    :last-will-mount (goog/now)})\n      (.call f this))))\n\n(defn wrap-did-mount\n  \"Tracks last call time of componentDidMount for each component and updates\n   the render times (using start time provided by wrap-will-mount), then\n   calls the original componentDidMount.\"\n  [f]\n  (fn []\n    (this-as this\n      (swap! component-stats update-in [((aget this \"getDisplayName\"))]\n             (fn [stats]\n               (let [now (goog/now)]\n                 (-> stats\n                   (assoc :last-did-mount now)\n                   (update-in [:mount-ms] (fnil conj [])\n                              (max (- now (:last-will-mount stats now)) 0))))))\n      (.call f this))))\n\n(defn instrument-methods [methods]\n  (-> methods\n    (update-in [:componentWillUpdate] wrap-will-update)\n    (update-in [:componentDidUpdate] wrap-did-update)\n    (update-in [:componentWillMount] wrap-will-mount)\n    (update-in [:componentDidMount] wrap-did-mount)))\n\n(def instrumentation-methods\n  (om/specify-state-methods!\n   (-> om/pure-methods\n     (instrument-methods)\n     (clj->js))))\n\n(defn avg [coll]\n  (/ (reduce + coll)\n     (count coll)))\n\n(defn std-dev [coll]\n  (let [a (avg coll)]\n    (Math/sqrt (avg (map #(Math/pow (- % a) 2) coll)))))\n\n(defn compare-display-name [a b]\n  (compare (:display-name b)\n           (:display-name a)))\n\n(defn compare-last-update [a b]\n  (let [res (compare (max (:last-will-update a) (:last-will-mount a))\n                     (max (:last-will-update b) (:last-will-mount b)))]\n    (if (zero? res)\n      (compare-display-name a b)\n      res)))\n\n(defn format-shortcut [key-set]\n  (str/join \"+\" (sort-by (comp - count) key-set)))\n\n(defn stats-view [data owner {:keys [clear-shortcut toggle-shortcut sort-shortcut]}]\n  (reify\n    om/IDisplayName (display-name [_] \"Om Instrumentation\")\n    om/IInitState (init-state [_] {:shown? false\n                                   :sort-orders (cycle [:last-update :display-name\n                                                        :mount-count :render-count])})\n    om/IDidMount\n    (did-mount [_]\n      (keyboard/register-key-handler owner {clear-shortcut #(om/transact! data (constantly {}))\n                                            toggle-shortcut #(om/update-state! owner :shown? not)\n                                            sort-shortcut #(om/update-state! owner :sort-orders rest)}))\n    om/IWillUnmount\n    (will-unmount [_] (keyboard/dispose-key-handler owner))\n    om/IRenderState\n    (render-state [_ {:keys [shown? sort-orders]}]\n      (dom/figure nil\n        (when shown?\n          (let [sort-order (first sort-orders)\n                stats-compare (case sort-order\n                                :last-update compare-last-update\n                                :display-name compare-display-name\n                                (fn [x y] (compare (sort-order x) (sort-order y))))\n                stats (map (fn [[display-name renders]]\n                             (let [render-times (filter identity (mapcat :render-ms renders))\n                                   mount-times (filter identity (mapcat :mount-ms renders))]\n                               {:display-name (or display-name \"Unknown\")\n                                :render-count (count render-times)\n                                :mount-count (count mount-times)\n\n                                :last-will-update (last (sort (map :last-will-update renders)))\n                                :last-will-mount (last (sort (map :last-will-mount renders)))\n\n                                :last-render-ms (last (:render-ms (last (sort-by :last-did-update renders))))\n                                :last-mount-ms (last (:mount-ms (last (sort-by :last-did-mount renders))))\n\n                                :average-render-ms (when (seq render-times) (int (avg render-times)))\n                                :average-mount-ms (when (seq mount-times) (int (avg mount-times)))\n\n                                :max-render-ms (when (seq render-times) (apply max render-times))\n                                :max-mount-ms (when (seq mount-times) (apply max mount-times))\n\n                                :min-render-ms (when (seq render-times) (apply min render-times))\n                                :min-mount-ms (when (seq mount-times) (apply min mount-times))\n\n                                :render-std-dev (when (seq render-times) (int (std-dev render-times)))\n                                :mount-std-dev (when (seq mount-times) (int (std-dev mount-times)))}))\n                           (reduce (fn [acc [display-name data]]\n                                     (update-in acc [(:display-name data)] (fnil conj []) data))\n                                   {} data))]\n            (dom/table #js {:className \"instrumentation-table\"}\n              (dom/thead nil\n                (dom/tr nil\n                  (dom/th nil \"component\")\n                  (dom/th #js {:className \"number right\"} \"render \")\n                  (dom/th #js {:className \"number left\"} \"/ mount\")\n                  (dom/th #js {:className \"number\" :colSpan \"2\"} \"last-ms\")\n                  (dom/th #js {:className \"number\" :colSpan \"2\"} \"average-ms\")\n                  (dom/th #js {:className \"number\" :colSpan \"2\"} \"max-ms\")\n                  (dom/th #js {:className \"number\" :colSpan \"2\"} \"min-ms\")\n                  (dom/th #js {:className \"number\" :colSpan \"2\"} \"std-ms\")))\n              (apply dom/tbody nil\n                     (for [{:keys [display-name\n                                   last-will-update last-will-mount\n                                   average-render-ms average-mount-ms\n                                   max-render-ms max-mount-ms\n                                   min-render-ms min-mount-ms\n                                   render-std-dev mount-std-dev\n                                   render-count mount-count\n                                   last-render-ms last-mount-ms] :as stat}\n                           (reverse (sort stats-compare stats))]\n                       (dom/tr nil\n                         (dom/td nil display-name)\n                         (dom/td #js {:className \"number\" } render-count)\n                         (dom/td #js {:className \"number\" } (when mount-count (gstring/format \"%4d\" mount-count)))\n\n                         (dom/td #js {:className \"number\" } last-render-ms)\n                         (dom/td #js {:className \"number\" } (when last-mount-ms (gstring/format \"%3d\" last-mount-ms)))\n\n                         (dom/td #js {:className \"number\" } average-render-ms)\n                         (dom/td #js {:className \"number\" } (when average-mount-ms (gstring/format \"%3d\" average-mount-ms)))\n\n                         (dom/td #js {:className \"number\" } max-render-ms)\n                         (dom/td #js {:className \"number\" } (when max-mount-ms (gstring/format \"%3d\" max-mount-ms)))\n\n                         (dom/td #js {:className \"number\" } min-render-ms)\n                         (dom/td #js {:className \"number\" } (when min-mount-ms (gstring/format \"%3d\" min-mount-ms)))\n\n                         (dom/td #js {:className \"number\" } render-std-dev)\n                         (dom/td #js {:className \"number\" } (when mount-std-dev (gstring/format \"%3d\" mount-std-dev))))))\n              (dom/tfoot nil\n                (dom/tr nil\n                  (dom/td #js {:className \"instrumentation-info\" :colSpan \"13\"}\n                          (gstring/format \"Component render stats, sorted by %s (%s). Clicks go through. %s to toggle, %s to clear.\"\n                                          sort-order\n                                          (format-shortcut sort-shortcut)\n                                          (format-shortcut toggle-shortcut)\n                                          (format-shortcut clear-shortcut))))))))))))\n\n(defn prepend-stats-node [classname]\n  (let [node (goog.dom/htmlToDocumentFragment (gstring/format \"<div class='%s'></div>\" classname))\n        body js/document.body]\n    (.insertBefore body node (.-firstChild body))\n    node))\n\n(defn setup-component-stats!\n  ([]\n   (setup-component-stats! {}))\n  ([{:keys [class clear-shortcut toggle-shortcut sort-shortcut]\n     :or {class \"om-instrumentation\"\n          clear-shortcut #{\"shift\" \"ctrl\" \"alt\" \"k\"}\n          toggle-shortcut #{\"shift\" \"ctrl\" \"alt\" \"j\"}\n          sort-shortcut #{\"shift\" \"ctrl\" \"alt\" \"s\"}}}]\n   (let [stats-node (or (goog.dom/getElementByClass class)\n                        (prepend-stats-node class))]\n     (om/root\n      stats-view\n      component-stats\n      {:target stats-node\n       :opts {:clear-shortcut clear-shortcut\n              :toggle-shortcut toggle-shortcut\n              :sort-shortcut sort-shortcut}}))))\n"
  },
  {
    "path": "src/om_i/hacks.cljs",
    "content": "(ns om-i.hacks\n  (:require [goog.dom :as dom]))\n\n(def om-i-css \"@keyframes in-fade-top-soft{0%{opacity:0;transform:translate3d(0, -4rem, 0)}100%{opacity:1;transform:none}}@-webkit-keyframes in-fade-top-soft{0%{opacity:0;-webkit-transform:translate3d(0, -4rem, 0)}100%{opacity:1;-webkit-transform:none}}.om-instrumentation{user-select:none;-moz-user-select:none;-webkit-user-select:none;pointer-events:none;color:#888;position:fixed;z-index:1000;top:0;left:0;width:100%}.instrumentation-table{-webkit-animation:in-fade-top-soft 500ms;animation:in-fade-top-soft 500ms;background-color:rgba(0,0,0,0.6);font-family:Monaco,monospace;font-size:.75rem;line-height:2;width:100%}.instrumentation-table th{color:#fff;line-height:1rem;padding:1.5rem 0;text-transform:uppercase;text-align:left}.instrumentation-table th:not(:first-child){text-align:center}.instrumentation-table th.left{text-align:left}.instrumentation-table th.right{text-align:right}.instrumentation-table th,.instrumentation-table td{white-space:pre-wrap}.instrumentation-table th:first-child,.instrumentation-table td:first-child{padding-left:1.5rem}.instrumentation-table tbody tr:nth-child(odd){background-color:rgba(0,0,0,0.4)}.instrumentation-table tbody td:nth-child(even){text-align:right;border-right:1px dashed;padding-right:.5em}.instrumentation-table tbody td:nth-child(odd){text-align:left;padding-left:.5em}.instrumentation-table tbody td:first-child{padding-left:1.5rem}.instrumentation-table tfoot td{line-height:1rem;text-align:center;padding:1.5rem 0}.instrumentation-table small{font-size:1em;opacity:.5}\")\n\n(defn insert-styles\n  \"This shouldn't be used in real code, but it can be useful when\n   exploring the code for the first time. Closure will eliminate this\n   function in production unless you're using it in production. You're not\n   using it in production, are you?\"\n  []\n  (let [s (goog.dom/createElement \"style\")]\n    (goog.dom/setTextContent s om-i-css)\n    (goog.dom/appendChild js/document.head s)))\n"
  },
  {
    "path": "src/om_i/keyboard.cljs",
    "content": "(ns om-i.keyboard\n  (:require [goog.events]\n            [om.core :as om])\n  (:import [goog.ui IdGenerator]))\n\n(def code->key\n  \"map from a character code (read from events with event.which)\n  to a string representation of it.\n  Only need to add 'special' things here.\"\n  {  8 \"backspace\"\n    13 \"enter\"\n    16 \"shift\"\n    17 \"ctrl\"\n    18 \"alt\"\n    27 \"esc\"\n    33 \"pageup\"\n    34 \"pagedown\"\n    36 \"home\"\n    37 \"left\"\n    38 \"up\"\n    39 \"right\"\n    40 \"down\"\n    46 \"del\"\n    91 \"meta\"\n    32 \"space\"\n   186 \";\"\n   191 \"/\"\n   219 \"[\"\n   221 \"]\"\n   187 \"=\"\n   189 \"-\"\n   190 \".\"\n   220 \"\\\\\"})\n\n(def mod-translation\n  {\"shiftKey\" \"shift\"\n   \"altKey\"   \"alt\"\n   \"ctrlKey\"  \"ctrl\"\n   \"metaKey\"  \"meta\"})\n\n(defn event-modifiers\n  \"Given a keydown event, return the set of modifier keys that were being held.\"\n  [e]\n  (reduce (fn [acc [modifier key-name]]\n            (if (aget e modifier)\n              (conj acc key-name)\n              acc))\n          #{} mod-translation))\n\n(defn event->key-set\n  \"Given an event, return a set of keys string like #{\\\"up\\\"} or #{\\\"shift\\\" \\\"l\\\"}\n  describing the keys that were pressed. Will return lone modifier keys, like shift or ctrl\"\n  [e]\n  (let [code (.-keyCode e)\n        key (or (code->key code) (.toLowerCase (js/String.fromCharCode code)))]\n    (conj (event-modifiers e) key)))\n\n(defn match-keys\n  \"Given a keymap for the component and the most recent series of keys\n  that were pressed (not the codes, but sets of keys like #{'shift' 'r'})\n  return a handler fn associated with a key combo in the keys\n  list or nil.\"\n  [keymap keys]\n  (->> keymap\n    (keep (fn [[key-set f]]\n            (when (= keys key-set) f)))\n    first))\n\n(defn register-key-handler [owner keymap]\n  (om/set-state-nr! owner ::event-key\n    (goog.events/listen\n     js/window\n     \"keydown\"\n     (fn [e]\n       (when-let [f (match-keys keymap (event->key-set e))]\n         (f))))))\n\n(defn dispose-key-handler [owner]\n  (goog.events/unlistenByKey (om/get-state owner ::event-key)))\n"
  }
]