Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
821 lines (694 sloc) 24.1 KB
(ns ahubu.lib
(:require
[clojure.string :as str])
(:import
MyEventDispatcher
WebUIController
(java.io File)
(java.net HttpURLConnection URL URLConnection URLStreamHandler URLStreamHandlerFactory)
(javafx.application Application Platform)
(javafx.beans.value ChangeListener)
(javafx.concurrent Worker$State)
(javafx.event EventHandler)
(javafx.fxml FXMLLoader)
(javafx.scene Parent Scene)
(javafx.scene.control Label)
(javafx.scene.input Clipboard ClipboardContent KeyEvent)
(javafx.scene.web WebView)
(javafx.stage Stage)
(javax.net.ssl HttpsURLConnection)
(netscape.javascript JSObject)
(sun.net.www.protocol.https Handler)
))
(gen-class
:extends javafx.application.Application
:name com.ahungry.Browser)
(declare delete-current-scene)
(declare bind-keys)
(declare new-scene)
(declare goto-scene)
(declare hide-buffers)
(declare show-buffers)
(declare filter-buffers)
(declare omnibar-load-url)
(declare default-mode)
(declare omnibar-handler)
(declare omnibar-parse-command)
(declare omnibar-handle-command)
(defmacro compile-time-slurp [file]
(slurp file))
(def js-bundle (slurp "js-src/bundle.js"))
(defmacro run-later [& forms]
`(let [
p# (promise)
]
(Platform/runLater
(fn []
(deliver p# (try ~@forms (catch Throwable t# t#)))))
p#))
(def world
(atom
{
:cross-domain-url ""
:default-url (format "file://%s/docs/index.html" (System/getProperty "user.dir"))
:hinting? false
:mode :default
:new-tab? false
:omnibar-open? false
:scene-id 0
:scenes []
:searching? false
:showing-buffers? false
:stage nil
}))
(defn set-mode [mode]
(swap! world conj {:mode mode}))
(defn set-atomic-stage [stage]
(swap! world conj {:stage stage}))
(defn get-atomic-stage [] (:stage @world))
;; Each scene is basically a tab
(defn add-scene [scene]
(swap! world conj {:scenes (conj (:scenes @world) scene)}))
(defn get-scene [n]
(-> (:scenes @world) (get n)))
(defn get-scenes [] (:scenes @world))
(defn delete-nth-scene [scenes n]
(into []
(concat (subvec scenes 0 n)
(subvec scenes (+ 1 n) (count scenes)))))
(defn del-scene [n]
(swap! world conj {:scenes (-> (:scenes @world) (delete-nth-scene n))}))
(defn set-scene-id [n] (swap! world conj {:scene-id n}))
(defn get-scene-id [] (:scene-id @world))
(defn set-new-tab [b]
(swap! world conj {:new-tab? b}))
(defn get-new-tab? [] (:new-tab? @world))
;; (def atomic-default-url (atom "http://ahungry.com"))
(defn set-default-url [s]
(swap! world conj {:default-url s}))
(defn get-default-url [] (:default-url @world))
(defn set-showing-buffers [b]
(swap! world conj {:showing-buffers? b}))
(defn get-showing-buffers? [] (:showing-buffers? @world))
(defn get-omnibar []
(-> (get-scene-id) get-scene (.lookup "#txtURL")))
(defn get-webview []
(-> (get-scene-id) get-scene (.lookup "#webView")))
(defn get-webengine []
(-> (get-webview) .getEngine))
(defn get-buffers []
(-> (get-scene-id) get-scene (.lookup "#buffers")))
(defn get-tip []
(-> (get-scene-id) get-scene (.lookup "#tip")))
(defn set-tip [s]
(let [style (case s
"NORMAL" "-fx-text-fill: #af0; -fx-background-color: #000;"
"OMNI" "-fx-text-fill: #000; -fx-background-color: #36f"
"GO" "-fx-text-fill: #000; -fx-background-color: #f69"
"INSERT" "-fx-text-fill: #000; -fx-background-color: #f36"
"HINTING" "-fx-text-fill: #000; -fx-background-color: #f63"
"SEARCHING" "-fx-text-fill: #000; -fx-background-color: #f33"
"BUFFERS" "-fx-text-fill: #000; -fx-background-color: #63f"
"-fx-text-fill: #000; -fx-background-color: #af0")]
(run-later
(doto (get-tip)
(.setText s)
(.setStyle style)))))
(defn get-omnibar-text []
(-> (get-omnibar) .getText))
(defn set-omnibar-text [s]
(run-later
(doto (get-omnibar)
(.setText s)
(.positionCaret (count s)))))
(defn set-omnibar-text-to-url []
(when (not (:omnibar-open? @world))
(set-omnibar-text
(-> (get-webengine) .getLocation))))
(defn url-ignore-regexes-from-file [file]
(map re-pattern
(map #(format ".*%s.*" %)
(str/split (slurp file) #"\n"))))
(defn url-ignore-regexes []
(url-ignore-regexes-from-file "conf/url-ignore-regexes.txt"))
(defn matching-regexes [url regexes]
(filter #(re-matches % url) regexes))
(defn url-ignorable? [url]
(let [ignorables (matching-regexes url (url-ignore-regexes))]
(if (> (count ignorables) 0)
(do
(println (format "Ignoring URL: %s, hit %d matchers." url (count ignorables)))
true)
false)))
(defn get-base-domain-pattern [s]
(let [[_ fqdn] (re-matches #".*?://(.*?)[/.$]*" s)]
(if fqdn
(let [domain-parts (-> (str/split fqdn #"\.") reverse)
domain (-> (into [] domain-parts) (subvec 0 2))]
(if domain
(re-pattern
(format "^http[s]*://(.*\\.)*%s\\.%s/.*"
(second domain)
(first domain)))
#".*")) #".*")))
;; Work with a sort of timeout here - cross domain base is set strictly after
;; first URL request, then lax again after some time has expired.
;; FIXME: Handle root domain logic better - when to flip/flop cross domain setting
;; TODO: Add cross domain user setting
(defn block-cross-domain-net?x [url]
(let [domain (get-base-domain-pattern (:cross-domain-url @world))]
(swap! world conj {:cross-domain-url url})
(future (Thread/sleep 5000) (swap! world conj {:cross-domain-url ""}))
(if (not (re-matches (re-pattern domain) url))
(do (println (format "Blocking X-Domain request: %s" url))
(println domain)
true)
false)))
(defn block-cross-domain-net? [_ ] false)
(defn url-or-no [url proto]
(let [url (.toString url)]
(URL.
(if (or (url-ignorable? url) (block-cross-domain-net? url))
(format "%s://0.0.0.0:65535" proto)
url))))
;; Hmm, we could hide things we do not want to see.
(defn my-connection-handler [protocol]
(case protocol
"http" (proxy [sun.net.www.protocol.http.Handler] []
(openConnection [& [url proxy :as args]]
(println url)
(proxy-super openConnection (url-or-no url protocol) proxy)))
"https" (proxy [sun.net.www.protocol.https.Handler] []
(openConnection [& [url proxy :as args]]
(println url)
(proxy-super openConnection (url-or-no url protocol) proxy)))
nil
))
(defn quietly-set-cookies []
(def cookie-manager
(doto (java.net.CookieManager.)
java.net.CookieHandler/setDefault)))
(defn quietly-set-stream-factory []
(WebUIController/stfuAndSetURLStreamHandlerFactory)
;; (try
;; (def stream-handler-factory
;; (URL/setURLStreamHandlerFactory
;; (reify URLStreamHandlerFactory
;; (createURLStreamHandler [this protocol] (#'my-connection-handler protocol)))))
;; (catch Throwable e
;; ;; TODO: Attempt to force set with reflection maybe - although this is usually good enough.
;; ;; TODO: Make sure this isn't some big performance penalty.
;; ))
)
(defn -start [this stage]
(let [
root (FXMLLoader/load (-> "resources/WebUI.fxml" File. .toURI .toURL))
scene (Scene. root)
exit (reify javafx.event.EventHandler
(handle [this event]
(println "Goodbye")
(javafx.application.Platform/exit)
(System/exit 0)
))
]
(bind-keys stage)
(set-atomic-stage stage)
;; (set-scene-id 0)
;; (add-scene scene)
;; (bind-keys scene)
(doto stage
(.setOnCloseRequest exit)
(.setScene scene)
(.setTitle "AHUBU")
(.show))))
(defn execute-script [w-engine s]
(run-later
(let [
result (.executeScript w-engine s)
]
(if (instance? JSObject result)
(str result)
result))))
(defn dojs [s ]
(execute-script (get-webengine) s))
(defn dojsf [file]
(execute-script (get-webengine) (slurp (format "js-src/%s.js" file))))
(defn decrease-font-size []
(dojsf "decrease-font-size"))
(defn increase-font-size []
(dojsf "increase-font-size"))
(defn inject-firebug [w-engine]
(execute-script w-engine (slurp "js-src/inject-firebug.js")))
(defn execute-script-async [w-engine s]
(let [
p (promise)
*out* *out*
]
(Platform/runLater
(fn []
(let [
o (.executeScript w-engine "new Object()")
]
(.setMember o "cb" (fn [s] (deliver p s)))
(.setMember o "println" (fn [s] (println s)))
(.eval o s))))
@p))
(defn repl [webengine]
(let [s (read-line)]
(when (not= "" (.trim s))
(println @(execute-script webengine s))
(recur webengine))))
(defn bind [s obj webengine]
(run-later
(.setMember
(.executeScript webengine "window")
s obj)))
(defn clear-cookies [cookie-manager]
(-> cookie-manager .getCookieStore .removeAll))
(defn async-load [url]
(run-later
(doto (get-webengine)
(.load url))))
(defn async-loadx [url]
(let [
webengine (get-webengine)
p (promise)
f (fn [s]
(binding [*out* *out*] (println s)))
listener (reify ChangeListener
(changed [this observable old-value new-value]
(when (= new-value Worker$State/SUCCEEDED)
;; ;first remove this listener
;; (.removeListener observable this)
(println "In the ChangeListener...")
(execute-script webengine js-bundle)
;and then redefine log and error (fresh page)
(bind "println" f webengine)
(future
(Thread/sleep 1000)
(execute-script webengine "console.log = function(s) {println.invoke(s)};
console.error = function(s) {println.invoke(s)};
"))
(deliver p true))))
]
(run-later
(doto webengine
(-> .getLoadWorker .stateProperty (.addListener listener))
(.load url)))
@p))
(defn back [webengine]
(execute-script webengine "window.history.back()"))
(defn prev-scene []
(default-mode)
(let [n (get-scene-id)
id (- n 1)]
(if (< id 0)
(goto-scene (- (count (get-scenes)) 1))
(goto-scene id))))
(defn next-scene []
(default-mode)
(let [n (get-scene-id)
id (+ n 1)]
(if (>= id (count (get-scenes)))
(goto-scene 0)
(goto-scene id))))
(defn omnibar-stop []
(swap! world conj {:omnibar-open? false})
(run-later
(future (Thread/sleep 100) (set-omnibar-text-to-url))
(doto (get-omnibar) (.setDisable true))
(doto (get-webview) (.setDisable false))))
(defn omnibar-start []
(swap! world conj {:omnibar-open? true})
(run-later
(doto (get-omnibar) (.setDisable false) (.requestFocus))
(doto (get-webview) (.setDisable true))))
(defn yank [s]
(let [content (ClipboardContent.)]
(run-later
(set-tip "YANKED!")
(future (Thread/sleep 500) (set-tip "NORMAL"))
(-> content (.putString s))
(-> (Clipboard/getSystemClipboard) (.setContent content)))))
(defn yank-current-url []
(-> (get-webengine) .getLocation yank))
(defn buffers-start []
(set-mode :omnibar)
(set-tip "BUFFERS")
(set-showing-buffers true)
(run-later
(omnibar-start)
(show-buffers)
(set-omnibar-text ":buffers! ")
"Overlay.show()"))
(defn quickmark-url [url]
(default-mode)
(omnibar-load-url url))
(defn get-xdg-config-home []
(or (System/getenv "XDG_CONFIG_HOME")
(System/getProperty "user.home")))
(defn get-rc-file-raw []
(let [defaults (read-string (slurp "conf/default-rc"))
home-rc (format "%s/.ahuburc" (System/getProperty "user.home"))
xdg-rc (format "%s/ahubu/ahuburc" (get-xdg-config-home))]
(conj
defaults
(if (.exists (clojure.java.io/file home-rc))
(read-string (slurp home-rc)))
(if (.exists (clojure.java.io/file xdg-rc))
(read-string (slurp xdg-rc))))))
(defn get-rc-file []
(let [rc (get-rc-file-raw)
quickmarks (:quickmarks rc)
qm-fns (reduce-kv #(assoc %1 %2 (fn [] (quickmark-url %3))) {} quickmarks)
merged-qms (conj (:quickmarks (:keymaps rc)) qm-fns)]
(conj rc
{:keymaps (conj (:keymaps rc)
{:quickmarks merged-qms})})))
(defn go-mode []
(set-mode :go)
(set-tip "GO"))
(defn font-mode []
(set-mode :font)
(set-tip "FONT"))
(defn quickmarks-mode []
(set-mode :quickmarks)
(set-tip "QUICKMARKS"))
(defn quickmarks-new-tab-mode []
(set-new-tab true)
(quickmarks-mode))
(defn default-mode []
(set-mode :default)
(set-tip "NORMAL")
(hide-buffers)
(omnibar-stop)
(swap! world conj {:hinting? false :searching? false})
(dojs "Hinting.off(); Overlay.hide(); Form.disable()"))
(defn insert-mode []
(set-mode :insert)
(set-tip "INSERT")
(dojs "Form.enable()"))
(defn search-mode []
(set-mode :search)
(set-tip "SEARCHING")
(swap! world conj {:searching? true})
(println "Searching")
(dojs "Search.reset()"))
(defn hinting-mode []
(set-mode :hinting)
(set-tip "HINTING")
(swap! world conj {:hinting? true})
(dojs "Hinting.on(); Overlay.show()"))
(defn inject-firebug []
(dojsf "inject-firebug"))
(defn omnibar-open []
(set-mode :omnibar)
(set-tip "OMNI")
(omnibar-start)
(set-omnibar-text ":open ")
(dojs "Overlay.show()"))
(defn omnibar-open-current []
(omnibar-open)
(set-omnibar-text (format ":open %s" (get-omnibar-text))))
(defn omnibar-open-new-tab []
(set-new-tab true)
(omnibar-open)
(set-omnibar-text ":tabopen "))
(defn go-top []
(default-mode)
(dojs "window.scrollTo(0, 0)"))
;; Try to grab string key, then keyword key
(defn key-map-op [key]
(let [mode (:mode @world)
rc (-> (:keymaps (get-rc-file)) (get mode))
op? (get rc key)
key (keyword key)
op (or op? (get rc key))]
op))
(defn process-op [op]
(when op
(if (= java.lang.String (type op))
(execute-script (get-webengine) op)
((eval op)))))
(defn key-map-handler [key]
(let [op (key-map-op key)
op-before (key-map-op :BEFORE)
op-after (key-map-op :AFTER)]
;; (println (format "KM OP: %s" op-before))
(println key)
(println (format "KM OP: %s" op))
;; (println (format "KM OP: %s" op-after))
;; Global key listeners
(when (get-showing-buffers?)
(filter-buffers))
(when (:hinting? @world)
(dojs (format "Hinting.keyHandler('%s')" key))
;; (println (format "HINTING: %s" key))
)
(when (:searching? @world)
(dojs (format "Search.incrementalFind('%s')" key))
)
;; Check for the BEFORE bind (runs with any other keypress)
(process-op op-before)
(process-op op)
(future
(Thread/sleep 100)
(process-op op-after))
true)) ; bubble up keypress
;; ENTER (code) vs <invis> (char), we want ENTER
;; Ideally, we want the char, since it tracks lowercase etc.
(defn get-readable-key [code text]
(if (>= (count text) (count code))
text code))
;; https://docs.oracle.com/javafx/2/events/filters.htm
(defn bind-keys [what]
(doto what
(->
(.addEventFilter
(. KeyEvent KEY_PRESSED)
(reify EventHandler ;; EventHandler
(handle [this event]
(let [ecode (-> event .getCode .toString)
etext (-> event .getText .toString)]
;; (println (get-readable-key ecode etext))
;; (.consume event)
;; disable webview here, until some delay was met
;; https://stackoverflow.com/questions/27038443/javafx-disable-highlight-and-copy-mode-in-webengine
;; https://docs.oracle.com/javase/8/javafx/api/javafx/scene/web/WebView.html
(key-map-handler (get-readable-key ecode etext)))
false
))))))
(defn show-alert [s]
(doto (javafx.scene.control.Dialog.)
(-> .getDialogPane (.setContentText s))
(-> .getDialogPane .getButtonTypes (.add (. javafx.scene.control.ButtonType OK)))
(.showAndWait)))
(defn goto-scene [n]
(println "GOING TO SCENE")
(println n)
(run-later
(set-scene-id n)
(doto (get-atomic-stage)
(.setScene (get-scene n))
(.show))))
(defn delete-current-scene []
(let [n (get-scene-id)]
(when (> n 0)
(goto-scene (- n 1))
(run-later
(Thread/sleep 50)
(del-scene n)))))
(defn omnibar-load-url [url]
(run-later
(if (get-new-tab?)
(do
(set-default-url url)
(new-scene)
(set-new-tab false))
(-> (get-webengine) (.load url)))))
(defn get-selected-buffer-text []
(let [bufs (get-buffers)
children (-> bufs .getChildren)
id 0
child (when children (get (vec children) id))]
(if child (.getText child) "")))
(defn switch-to-buffer []
(let [s (get-selected-buffer-text)
maybe-id (last (re-matches #"^([0-9]).*" s))
id (if maybe-id (Integer/parseInt maybe-id) -1)]
(when (>= id 0)
(goto-scene id))
(set-showing-buffers false)
(hide-buffers)))
(defn omnibar-parse-command [cmd]
(re-matches #":(.*?) (.*)" cmd))
(defn omnibar-handle-command [cmd]
(let [[_ cmd arg] (omnibar-parse-command cmd)]
(println (format "OB Parse Cmd: %s %s %s" _ cmd arg))
(case cmd
"open" (omnibar-handler arg)
"tabopen" (omnibar-handler arg)
(omnibar-handler _))))
(defn omnibar-handler [n]
(if (get-showing-buffers?) (switch-to-buffer)
(let [query
(cond
(re-matches #"^:.*" n) (omnibar-handle-command n)
(re-matches #"^file:.*" n) n
(re-matches #"^http[s]*:.*" n) n
(re-matches #".*\..*" n) (format "http://%s" n)
:else (format "https://duckduckgo.com/lite/?q=%s" n)
)]
(omnibar-load-url query))))
(defn hide-buffers []
(let [bufs (get-buffers)]
(run-later
(-> bufs .getChildren .clear))))
(defn is-matching-buf? [s]
(let [[_ cmd arg] (-> (get-omnibar) .getText omnibar-parse-command)
ob-text (or arg _)
pattern (re-pattern (str/lower-case (str/join "" [".*" ob-text ".*"])))]
(re-matches pattern (str/lower-case s))))
(defn get-buffer-entry-text [scene n]
(let [webview (.lookup scene "#webView")
engine (-> webview .getEngine)
title (-> engine .getTitle)
location (-> engine .getLocation)]
(format "%s :: %s :: %s" n title location)))
(defn filter-buffers []
(future
;; (Thread/sleep 100)
(let [bufs (get-buffers)
children (-> bufs .getChildren)]
(doall
(map
(fn [c]
(when (not (is-matching-buf? (.getText c)))
(run-later
(.remove children c)
)))
children)))))
(defn show-buffers []
(let [scenes (get-scenes)]
(run-later
(let [bufs (get-buffers)]
(doto bufs
(-> .getChildren .clear)
(-> .getChildren (.add (Label. "Buffers: "))))))
(doall
(map (fn [i]
(let [scene (get scenes i)]
(println "Make the scene....")
(run-later
(doto (-> (get-scene-id) get-scene (.lookup "#buffers"))
(-> .getChildren (.add (Label. (get-buffer-entry-text scene i))))))))
(range (count scenes))))))
;; Map over elements (links) on page load...sweet
;; TODO: Based on this filter list, we can show the user a native list
;; of jumpable links (instead of relying on JS), where it works like the buffer
;; jump list, but the action is set to .load url or simulate a key click
(defn el-link-fn [els]
(doall
(map (fn [i]
(let [el (-> els (.item i))]
;; https://docs.oracle.com/cd/E13222_01/wls/docs61/xerces/org/apache/html/dom/HTMLAnchorElementImpl.html
;; (-> el (.setTextContent "OH WEL"))
;; (println (-> el .getTextContent))
;; (println (-> el (.getAttribute "href")))
(-> el (.addEventListener
"click"
(reify org.w3c.dom.events.EventListener
(handleEvent [this event]
(default-mode)
(println "I clicked a link, good job")
(println (-> el .getTextContent))))
false))
)
)
(range (.getLength els)))))
(defn remove-annoying-div [dom id]
(let [el (-> dom (.getElementById id))]
(when el (.remove el))))
(defn remove-annoying-divs [dom]
(let [ids (str/split (slurp "conf/dom-id-ignores.txt") #"\n")]
(doall
(map #(remove-annoying-div dom %) ids))))
(defn remove-annoying-class [dom class-name]
(let [els (-> dom (.getElementsByClassName class-name))]
(doall
(map
(fn [_]
;; We remove item 0, because each remove causes a reindex
(let [el (-> els (.item 0))]
(-> el .remove)))
(range (.getLength els))))))
(defn remove-annoying-classes [dom]
(let [ids (str/split (slurp "conf/dom-class-ignores.txt") #"\n")]
(doall
(map #(remove-annoying-class dom %) ids))))
(defn new-scene []
(run-later
(let [
root (FXMLLoader/load (-> "resources/WebUI.fxml" File. .toURI .toURL))
scene (Scene. root)
]
(add-scene scene)
(set-scene-id (- (count (get-scenes)) 1))
;; (bind-keys scene)
;; (set-scene-id (+ 1 (get-scene-id)))
(println "Getting new scene, binding keys...")
;; Bind the keys
(let [webview (.lookup scene "#webView")
webengine (.getEngine webview)]
;; Clean up this mess
(doto webengine
;; (.onStatusChanged
;; (reify javafx.event.EventHandler
;; (handle [this event]
;; (println "On status change"))))
(.setOnAlert
(reify javafx.event.EventHandler
(handle [this event]
(println (.getData event))
(show-alert (.getData event)))))
(-> .getLoadWorker
.stateProperty
(.addListener
(reify ChangeListener
(changed [this observable old-value new-value]
(when (and (= new-value Worker$State/RUNNING)
(= old-value Worker$State/SCHEDULED))
(println "The running and schedule change")
(execute-script webengine js-bundle))
(when (not (= new-value Worker$State/SUCCEEDED))
(set-omnibar-text
(format "Loading :: %s" (-> webengine .getLocation))))
(when (= new-value Worker$State/SUCCEEDED)
;; (.removeListener observable this)
(println "In boot change listener")
;; https://docs.oracle.com/javase/8/javafx/api/javafx/scene/web/WebEngine.html
(println (-> webengine .getLocation))
;; (println (-> webengine .getDocument .toString))
;; When a thing loads, set the URL to match
(set-omnibar-text-to-url)
;; map over all the page links on load
(-> webengine .getDocument remove-annoying-divs)
(-> webengine .getDocument remove-annoying-classes)
(-> webengine .getDocument (.getElementsByTagName "a") el-link-fn)
(-> webengine (.setUserAgent "Mozilla/5.0 (Windows NT 6.1) Gecko/20100101 Firefox/61.0"))
;; (-> webengine .getDocument (.getElementById "content")
;; (.addEventListener
;; "click"
;; (reify org.w3c.dom.events.EventListener
;; (handleEvent [this event]
;; (javafx.application.Platform/exit)))))
(execute-script webengine js-bundle)
)))))
(.load (get-default-url))
))
;; Add it to the stage
(doto (get-atomic-stage)
(.setScene scene)
(.show)))))
;; Abstract the webview + webengine
;; (-> (-> (get (ahubu.browser/get-scenes) 0) (.lookup "#webView")) .getEngine)