(ns frontend.handler.command-palette "System-component-like ns for command palette's functionality" (:require [cljs.spec.alpha :as s] [frontend.modules.shortcut.data-helper :as shortcut-helper] [frontend.spec :as spec] [frontend.state :as state] [lambdaisland.glogi :as log] [frontend.storage :as storage])) (s/def :command/id keyword?) (s/def :command/desc string?) (s/def :command/action fn?) (s/def :command/shortcut string?) (s/def :command/tag vector?) (s/def :command/command (s/keys :req-un [:command/id :command/action] ;; :command/desc is optional for internal commands since view ;; checks translation ns first :opt-un [:command/desc :command/shortcut :command/tag])) (defn global-shortcut-commands [] (->> [:shortcut.handler/editor-global :shortcut.handler/global-prevent-default :shortcut.handler/global-non-editing-only] (mapcat shortcut-helper/shortcuts->commands))) (defn get-commands [] (->> (get @state/state :command-palette/commands) (sort-by :id))) (defn get-commands-unique [] (reduce #(assoc %1 (:id %2) %2) {} (get @state/state :command-palette/commands))) (defn history ([] (or (storage/get "commands-history") [])) ([vals] (storage/set "commands-history" vals))) (defn- assoc-invokes [cmds] (let [invokes (->> (history) (map :id) (frequencies))] (mapv (fn [{:keys [id] :as cmd}] (if (contains? invokes id) (assoc cmd :invokes-count (get invokes id)) cmd)) cmds))) (defn add-history [{:keys [id]}] (storage/set "commands-history" (conj (history) {:id id :timestamp (.getTime (js/Date.))}))) (defn invoke-command [{:keys [action] :as cmd}] (add-history cmd) (state/set-state! :ui/command-palette-open? false) (state/close-modal!) (action)) (defn top-commands [limit] (->> (get-commands) (assoc-invokes) (sort-by :invokes-count) (reverse) (take limit))) (defn register "Register a global command searchable by command palette. `id` is defined as a global unique namespaced key :scope/command-name `action` must be a zero arity function Example: ```clojure (register {:id :document/open-logseq-doc :desc \"Document: open Logseq documents\" :action (fn [] (js/window.open \"https://docs.logseq.com/\"))}) ``` To add i18n support, prefix `id` with command and put that item in dict. Example: {:zh-CN {:command.document/open-logseq-doc \"打开文档\"}}" [{:keys [id] :as command}] (spec/validate :command/command command) (let [cmds (get-commands)] (if (some (fn [existing-cmd] (= (:id existing-cmd) id)) cmds) (log/error :command/register {:msg "Failed to register command. Command with same id already exist" :id id}) (state/set-state! :command-palette/commands (conj cmds command))))) (defn unregister [id] (let [id (keyword id) cmds (get-commands-unique)] (when (contains? cmds id) (state/set-state! :command-palette/commands (vals (dissoc cmds id))) ;; clear history (history (filter #(not= (:id %) id) (history)))))) (defn register-global-shortcut-commands [] (let [cmds (global-shortcut-commands)] (doseq [cmd cmds] (register cmd)))) (comment ;; register custom command example (register {:id :document/open-logseq-doc :desc "Document: open Logseq documents" :action (fn [] (js/window.open "https://docs.logseq.com/"))}))