diff --git a/externs.js b/externs.js index 090f1ea5a..c049ac223 100644 --- a/externs.js +++ b/externs.js @@ -115,6 +115,9 @@ dummy.getPageView = function() {}; dummy.convertToPdfPoint = function() {}; dummy.scrollPageIntoView = function() {}; dummy.convertToViewportRectangle = function() {}; +dummy.init = function() {}; +dummy.commit = function() {}; +dummy.raw = function() {}; /** * @typedef {{ diff --git a/resources/package.json b/resources/package.json index 9096034b3..5e1da561a 100644 --- a/resources/package.json +++ b/resources/package.json @@ -27,7 +27,8 @@ "node-fetch": "^2.6.1", "open": "^7.3.1", "semver": "^7.3.5", - "update-electron-app": "^2.0.1" + "update-electron-app": "^2.0.1", + "simple-git": "2.44.0" }, "devDependencies": { "@electron-forge/cli": "^6.0.0-beta.57", diff --git a/src/electron/electron/core.cljs b/src/electron/electron/core.cljs index 0bcb3af58..174a42993 100644 --- a/src/electron/electron/core.cljs +++ b/src/electron/electron/core.cljs @@ -11,7 +11,8 @@ ["electron" :refer [BrowserWindow app protocol ipcMain dialog Menu MenuItem] :as electron] ["electron-window-state" :as windowStateKeeper] [clojure.core.async :as async] - [electron.state :as state])) + [electron.state :as state] + [electron.git :as git])) (def MAIN_WINDOW_ENTRY (if dev? "http://localhost:3001" @@ -224,6 +225,8 @@ (search/open-dbs!) + (git/auto-commit-current-graph!) + (vreset! *setup-fn (fn [] (let [t0 (setup-updater! win) diff --git a/src/electron/electron/git.cljs b/src/electron/electron/git.cljs new file mode 100644 index 000000000..cd5457edb --- /dev/null +++ b/src/electron/electron/git.cljs @@ -0,0 +1,113 @@ +(ns electron.git + (:require ["child_process" :as child-process] + ["simple-git" :as simple-git] + [goog.object :as gobj] + [electron.state :as state] + [electron.utils :as utils] + [promesa.core :as p] + [clojure.string :as string])) + +(def spawn-sync (gobj/get child-process "spawnSync")) + +(defonce gits + (atom {})) + +(defn installed? + [] + (let [command (spawn-sync "git" + #js ["--version"] + #js {:stdio "ignore"})] + (if-let [error (gobj/get command "error")] + (do + (js/console.error error) + false) + true))) + +(defn get-git + [] + (when (installed?) + (when-let [path (:graph/current @state/state)] + (if-let [result (get @gits path)] + result + (let [result (simple-git path)] + (swap! gits assoc path result) + result))))) + +(defn init! + [] + (when-let [git ^js (get-git)] + (.init git false))) + +(defn add-all! + ([] + (add-all! (get-git))) + ([^js git] + (when git + (.add git "./*" (fn [error] (js/console.error error)))))) + +(defn add-all-and-commit! + ([] + (add-all-and-commit! "Auto saved by Logseq")) + ([message] + (when-let [git ^js (get-git)] + (p/let [_ (add-all! git)] + (.commit git message))))) + +(defonce quotes-regex #"\"[^\"]+\"") +(defn wrapped-by-quotes? + [v] + (and (string? v) (>= (count v) 2) (= "\"" (first v) (last v)))) + +(defn unquote-string + [v] + (string/trim (subs v 1 (dec (count v))))) + +(defn- split-args + [s] + (let [quotes (re-seq quotes-regex s) + non-quotes (string/split s quotes-regex) + col (if (seq quotes) + (concat (interleave non-quotes quotes) + (drop (count quotes) non-quotes)) + non-quotes)] + (->> col + (map (fn [s] + (if (wrapped-by-quotes? s) + [(unquote-string s)] + (string/split s #"\s")))) + (flatten) + (remove string/blank?)))) + +(defn raw! + [args & {:keys [ok-handler error-handler]}] + (when-let [git ^js (get-git)] + (let [args (if (string? args) + (split-args args) + args) + ok-handler (if ok-handler + ok-handler + (fn [result] + (utils/send-to-renderer "notification" {:type "success" + :payload result}))) + error-handler (if error-handler + error-handler + (fn [error] + (js/console.dir error) + (utils/send-to-renderer "notification" {:type "error" + :payload (.toString error)})))] + (p/let [_ (when (= (first args) "commit") + (add-all!))] + (-> + (p/let [result (.raw git (clj->js args))] + (when ok-handler + (ok-handler result))) + (p/catch error-handler)))))) + +(defn auto-commit-current-graph! + [] + (when (installed?) + (state/clear-git-commit-interval!) + (p/let [_ (add-all-and-commit!)] + (let [seconds (state/get-git-commit-seconds) + interval (js/setInterval add-all-and-commit! (* seconds 1000))] + (state/set-git-commit-interval! interval))))) diff --git a/src/electron/electron/handler.cljs b/src/electron/electron/handler.cljs index 4ba2d5847..ed048ff6c 100644 --- a/src/electron/electron/handler.cljs +++ b/src/electron/electron/handler.cljs @@ -13,7 +13,8 @@ [electron.utils :as utils] [electron.state :as state] [clojure.core.async :as async] - [electron.search :as search])) + [electron.search :as search] + [electron.git :as git])) (defmulti handle (fn [_window args] (keyword (first args)))) @@ -198,6 +199,14 @@ (defmethod handle :getDirname [_] js/__dirname) +(defmethod handle :setCurrentGraph [_ [_ path]] + (let [path (when path (string/replace path "logseq_local_" ""))] + (swap! state/state assoc :graph/current path))) + +(defmethod handle :runGit [_ [_ args]] + (when (seq args) + (git/raw! args))) + (defmethod handle :default [args] (println "Error: no ipc handler for: " (bean/->js args))) diff --git a/src/electron/electron/state.cljs b/src/electron/electron/state.cljs index b10582b79..c67c78655 100644 --- a/src/electron/electron/state.cljs +++ b/src/electron/electron/state.cljs @@ -2,3 +2,34 @@ (:require [clojure.core.async :as async])) (defonce persistent-dbs-chan (async/chan 1)) + +(defonce state + (atom {:graph/current nil + :git/auto-commit-seconds 60 + :git/auto-commit-interval nil})) + +(defn set-state! + [path value] + (if (vector? path) + (swap! state assoc-in path value) + (swap! state assoc path value))) + +(defn set-git-commit-interval! + [v] + (set-state! :git/auto-commit-interval v)) + +(defn clear-git-commit-interval! + [] + (when-let [interval (get @state :git/auto-commit-interval)] + (js/clearInterval interval))) + +(defn set-git-commit-seconds! + [v] + (let [v (if (and (integer? v) (< 0 v (inc (* 60 10)))) ; max 10 minutes + v + 60)] + (set-state! :git/auto-commit-seconds v))) + +(defn get-git-commit-seconds + [] + (or (get @state :git/auto-commit-seconds) 60)) diff --git a/src/electron/electron/utils.cljs b/src/electron/electron/utils.cljs index 8b49f8fb2..3c27adc56 100644 --- a/src/electron/electron/utils.cljs +++ b/src/electron/electron/utils.cljs @@ -2,7 +2,9 @@ (:require [clojure.string :as string] ["fs" :as fs] ["path" :as path] - [clojure.string :as string])) + [clojure.string :as string] + [cljs-bean.core :as bean] + ["electron" :refer [BrowserWindow]])) (defonce mac? (= (.-platform js/process) "darwin")) (defonce win32? (= (.-platform js/process) "win32")) @@ -45,3 +47,13 @@ [path] (when (fs/existsSync path) (.toString (fs/readFileSync path)))) + +(defn get-focused-window + [] + (.getFocusedWindow BrowserWindow)) + +(defn send-to-renderer + [kind payload] + (when-let [window (get-focused-window)] + (.. ^js window -webContents + (send kind (bean/->js payload))))) diff --git a/src/main/electron/listener.cljs b/src/main/electron/listener.cljs index ac6f9d442..d70f0e192 100644 --- a/src/main/electron/listener.cljs +++ b/src/main/electron/listener.cljs @@ -11,22 +11,6 @@ [frontend.handler.metadata :as metadata-handler] [frontend.ui :as ui])) -(defn listen-to-open-dir! - [] - (js/window.apis.on "open-dir-confirmed" - (fn [] - (state/set-loading-files! true) - (when-not (state/home?) - (route-handler/redirect-to-home!))))) - -(defn run-dirs-watcher! - [] - ;; TODO: move "file-watcher" to electron.ipc.channels - (js/window.apis.on "file-watcher" - (fn [data] - (let [{:keys [type payload]} (bean/->clj data)] - (watcher-handler/handle-changed! type payload))))) - (defn listen-persistent-dbs! [] ;; TODO: move "file-watcher" to electron.ipc.channels @@ -54,8 +38,28 @@ 100)) (ipc/ipc "persistent-dbs-saved")))))) +(defn listen-to-electron! + [] + (js/window.apis.on "open-dir-confirmed" + (fn [] + (state/set-loading-files! true) + (when-not (state/home?) + (route-handler/redirect-to-home!)))) + + ;; TODO: move "file-watcher" to electron.ipc.channels + (js/window.apis.on "file-watcher" + (fn [data] + (let [{:keys [type payload]} (bean/->clj data)] + (watcher-handler/handle-changed! type payload)))) + + (js/window.apis.on "notification" + (fn [data] + (let [{:keys [type payload]} (bean/->clj data) + type (keyword type) + comp [:div (str payload)]] + (notification/show! comp type false))))) + (defn listen! [] - (listen-to-open-dir!) - (run-dirs-watcher!) + (listen-to-electron!) (listen-persistent-dbs!)) diff --git a/src/main/frontend/components/shell.cljs b/src/main/frontend/components/shell.cljs new file mode 100644 index 000000000..25feb1f39 --- /dev/null +++ b/src/main/frontend/components/shell.cljs @@ -0,0 +1,34 @@ +(ns frontend.components.shell + (:require [rum.core :as rum] + [frontend.ui :as ui] + [frontend.util :as util] + [frontend.handler.shell :as shell-handler] + [clojure.string :as string] + [frontend.mixins :as mixins])) + +(defn- run-command + [command] + (when-not (string/blank? @command) + (shell-handler/run-command! @command))) + +(defonce command (atom "")) +(rum/defcs shell < rum/reactive + (mixins/event-mixin + (fn [state] + (mixins/on-enter state + :on-enter (fn [state] + (run-command command))))) + [state] + [:div.flex.flex-col + [:div.w-full.mx-auto.sm:max-w-lg.sm:w-96 + [:div + [:div + [:h1.title + "Input command"] + [:div.mt-4.mb-4.relative.rounded-md.shadow-sm.max-w-xs + [:input#run-command.form-input.block.w-full.sm:text-sm.sm:leading-5 + {:autoFocus true + :placeholder "git ..." + :on-change (fn [e] + (reset! command (util/evalue e)))}]]]] + (ui/button "Run" :on-click #(run-command command))]]) diff --git a/src/main/frontend/format/block.cljs b/src/main/frontend/format/block.cljs index 0f64127c6..c54fc8ae1 100644 --- a/src/main/frontend/format/block.cljs +++ b/src/main/frontend/format/block.cljs @@ -269,16 +269,17 @@ (defn convert-page-if-journal "Convert journal file name to user' custom date format" [original-page-name] - (let [page-name (string/lower-case original-page-name) - day (date/journal-title->int page-name)] - (if day - (let [original-page-name (date/int->journal-title day)] - [original-page-name (string/lower-case original-page-name) day]) - [original-page-name page-name day]))) + (when original-page-name + (let [page-name (string/lower-case original-page-name) + day (date/journal-title->int page-name)] + (if day + (let [original-page-name (date/int->journal-title day)] + [original-page-name (string/lower-case original-page-name) day]) + [original-page-name page-name day])))) (defn page-name->map [original-page-name with-id?] - (when original-page-name + (when (and original-page-name (string? original-page-name)) (let [original-page-name (util/remove-boundary-slashes original-page-name) [original-page-name page-name journal-day] (convert-page-if-journal original-page-name) namespace? (and (string/includes? original-page-name "/") diff --git a/src/main/frontend/format/mldoc.cljs b/src/main/frontend/format/mldoc.cljs index 69720a957..377975e7d 100644 --- a/src/main/frontend/format/mldoc.cljs +++ b/src/main/frontend/format/mldoc.cljs @@ -152,7 +152,7 @@ properties-ast (map (fn [[k v]] (let [k (keyword (string/lower-case k)) - v (if (contains? #{:title :description :filters :roam_tags} k) + v (if (contains? #{:title :description :filters :roam_tags :macro} k) v (text/split-page-refs-without-brackets v))] [k v])))) @@ -162,10 +162,11 @@ (->> (map (fn [[_ v]] - (let [[k v] (util/split-first " " v)] - (mapv - string/trim - [k v]))) + (do + (let [[k v] (util/split-first " " v)] + (mapv + string/trim + [k v])))) macro-properties) (into {})) {}) diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index abeccc216..60061b97a 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -11,6 +11,7 @@ [frontend.handler.editor :as editor-handler] [frontend.handler.page :as page-handler] [frontend.components.encryption :as encryption] + [frontend.components.shell :as shell] [frontend.fs.nfs :as nfs] [frontend.db.conn :as conn] [frontend.extensions.srs :as srs] @@ -155,6 +156,10 @@ false)))) repos)) +(defmethod handle :command/run [_] + (when (util/electron?) + (state/set-modal! shell/shell))) + (defn run! [] (let [chan (state/get-events-chan)] diff --git a/src/main/frontend/handler/shell.cljs b/src/main/frontend/handler/shell.cljs new file mode 100644 index 000000000..ae7cf3e2b --- /dev/null +++ b/src/main/frontend/handler/shell.cljs @@ -0,0 +1,30 @@ +(ns frontend.handler.shell + (:require [electron.ipc :as ipc] + [clojure.string :as string] + [frontend.util :as util] + [frontend.handler.notification :as notification])) + +(defn run-git-command! + [command] + (ipc/ipc "runGit" command)) + +(defn run-pandoc-command! + [command] + (ipc/ipc "runPandoc" command)) + +(defn run-command! + [command] + (let [[command args] (util/split-first " " command) + command (and command (string/lower-case command))] + (when (and (not (string/blank? command)) (not (string/blank? args))) + (let [args (string/trim args)] + (case (keyword command) + :git + (run-git-command! args) + + :pandoc + (run-pandoc-command! args) + + (notification/show! + [:div (str command " is not supported yet!")] + :error)))))) diff --git a/src/main/frontend/modules/shortcut/config.cljs b/src/main/frontend/modules/shortcut/config.cljs index f387d9b2a..900341046 100644 --- a/src/main/frontend/modules/shortcut/config.cljs +++ b/src/main/frontend/modules/shortcut/config.cljs @@ -322,7 +322,11 @@ :shortcut.handler/global-non-editing-only ^{:before m/enable-when-not-editing-mode!} - {:ui/toggle-document-mode + {:command/run + {:desc "Run git/pandoc/others commands" + :binding "r" + :fn #(state/pub-event! [:command/run])} + :ui/toggle-document-mode {:desc "Toggle document mode" :binding "t d" :fn state/toggle-document-mode!} diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index 373f27d63..970e0c686 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -17,8 +17,11 @@ [cljs-time.format :as tf])) (defonce ^:private state - (atom - (let [document-mode? (or (storage/get :document/mode?) false)] + (let [document-mode? (or (storage/get :document/mode?) false) + current-graph (let [graph (storage/get :git/current-repo)] + (when graph (ipc/ipc "setCurrentGraph" graph)) + graph)] + (atom {:route-match nil :today nil :system/events (async/chan 100) @@ -38,7 +41,7 @@ :network/online? true :indexeddb/support? true :me nil - :git/current-repo (storage/get :git/current-repo) + :git/current-repo current-graph :git/status {} :format/loading {} :draw? false @@ -391,7 +394,8 @@ (swap! state assoc :git/current-repo repo) (if repo (storage/set :git/current-repo repo) - (storage/remove :git/current-repo))) + (storage/remove :git/current-repo)) + (ipc/ipc "setCurrentGraph" repo)) (defn set-preferred-format! [format] diff --git a/templates/config.edn b/templates/config.edn index 719732872..5ad4885bc 100644 --- a/templates/config.edn +++ b/templates/config.edn @@ -169,4 +169,8 @@ ;; hide specific properties for blocks ;; E.g. #{:created-at :updated-at} ;; :block-hidden-properties #{} + + ;; only for the desktop app + :git/auto-commit-seconds 60 + :git/disable-auto-commit? false }