init-eww.el 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. ;;; EWW
  2. ;; TODO: Fix `eww-forward-url' infinite forwarding.
  3. ;; TODO: Extend the history / bookmarks view to display tags, mark and search engine.
  4. ;; With colors: [mark] title url (tags)
  5. ;; Mark is red if no search engine, green otherwise.
  6. ;; Tags have their own face.
  7. ;; TODO: Make something useful with the tags. Helm function? Could chain two
  8. ;; functions: tag selection then filtered bookmark selection, then tag selection
  9. ;; again, etc.
  10. ;; Alternative: use special syntax in prompt, like find-files does. This does
  11. ;; not allow for listing tags though.
  12. (require 'helm-eww nil t)
  13. (setq eww-bookmarks-directory (expand-file-name "bookmarks" (getenv "PERSONAL")))
  14. ;; (add-to-list 'auto-mode-alist '("eww-bookmarks$" . emacs-lisp-mode))
  15. (defvar ambrevar/eww-bookmarks-file (expand-file-name "eww-bookmarks.gpg"
  16. eww-bookmarks-directory))
  17. (defun ambrevar/eww-copy-page-title ()
  18. "Copy the URL of the current page into the kill ring."
  19. (interactive)
  20. (message "%s" (plist-get eww-data :title))
  21. (kill-new (plist-get eww-data :title)))
  22. (defun ambrevar/eww-next-url (&optional backward)
  23. "Like `eww-next-url' but if no next URL is found, go to next URL numerically.
  24. The URL index is the last number after the last '/'."
  25. (interactive)
  26. (condition-case nil
  27. (if backward
  28. (eww-previous-url)
  29. (eww-next-url))
  30. (user-error
  31. (when (eq major-mode 'eww-mode)
  32. (require 'rx)
  33. (let* ((url (plist-get eww-data :url))
  34. (re (rx (group (one-or-more digit))
  35. (zero-or-more (not (any "/")))
  36. line-end)))
  37. (if (and (string-match re url)
  38. (or (not backward)
  39. (> (string-to-number (match-string 1 url)) 0)))
  40. (eww
  41. (replace-regexp-in-string
  42. re
  43. (format (format "%%0.%dd" (length (match-string 1 url))) ; In case matched number is zero-padded.
  44. (funcall (if backward '1- '1+) (string-to-number (match-string 1 url))))
  45. url nil nil 1))
  46. (message "No index in URL.")))))))
  47. (defun ambrevar/eww-previous-url ()
  48. "Like `eww-previous-url' but if no next URL is found, go to next URL numerically.
  49. The URL index is the last number after the last '/'."
  50. (interactive)
  51. (ambrevar/eww-next-url 'backward))
  52. (defun ambrevar/eww-reload-all (&optional buffers)
  53. "Like `eww-reload' but for multiple EWW BUFFERS.
  54. If BUFFERS is not specified, then reload all buffers."
  55. (interactive)
  56. (dolist (b (or buffers (buffer-list)))
  57. (with-current-buffer b
  58. (when (derived-mode-p 'eww-mode)
  59. (eww-reload)))))
  60. (defun ambrevar/eww (url)
  61. "Fetch URL and render the page.
  62. If the input doesn't look like an URL or a domain name, the
  63. word(s) will be searched for via `eww-search-prefix'."
  64. (interactive
  65. (let* ((uris (eww-suggested-uris))
  66. (prompt (concat "Enter URL or keywords: "))) ; PATCH
  67. (list (read-string prompt (car uris) nil uris)))) ; PATCH
  68. (setq url (eww--dwim-expand-url url))
  69. (pop-to-buffer-same-window
  70. (if (eq major-mode 'eww-mode)
  71. (current-buffer)
  72. (get-buffer-create "*eww*")))
  73. (eww-setup-buffer)
  74. ;; Check whether the domain only uses "Highly Restricted" Unicode
  75. ;; IDNA characters. If not, transform to punycode to indicate that
  76. ;; there may be funny business going on.
  77. (let ((parsed (url-generic-parse-url url)))
  78. (when (url-host parsed)
  79. (unless (puny-highly-restrictive-domain-p (url-host parsed))
  80. (setf (url-host parsed) (puny-encode-domain (url-host parsed)))
  81. (setq url (url-recreate-url parsed)))))
  82. (plist-put eww-data :url url)
  83. (plist-put eww-data :title "")
  84. (eww-update-header-line-format)
  85. (let ((inhibit-read-only t))
  86. (insert (format "Loading %s..." url))
  87. (goto-char (point-min)))
  88. (url-retrieve url 'eww-render
  89. (list url nil (current-buffer))))
  90. (advice-add 'eww :override 'ambrevar/eww)
  91. (defun ambrevar/eww-browse-with-external-browser (&optional url)
  92. "Browse the current URL with an external browser.
  93. The browser to used is specified by the `shr-external-browser' variable."
  94. (interactive)
  95. (funcall 'browse-url-default-browser (or url
  96. (car (eww-suggested-uris))
  97. (plist-get eww-data :url))))
  98. (advice-add 'eww-browse-with-external-browser
  99. :override
  100. 'ambrevar/eww-browse-with-external-browser)
  101. (defun ambrevar/eww-open-in-new-buffer (url)
  102. "Fetch URL and render the page.
  103. If the input doesn't look like an URL or a domain name, the
  104. word(s) will be searched for via `eww-search-prefix'."
  105. (interactive
  106. (let* ((uris (eww-suggested-uris))
  107. (prompt (concat "Open URL or keywords in new buffer: ")))
  108. (list (read-string prompt (car uris) nil uris))))
  109. (setq url (eww--dwim-expand-url url))
  110. (pop-to-buffer-same-window
  111. (if (eq major-mode 'eww-mode)
  112. (clone-buffer)
  113. (generate-new-buffer "*eww*")))
  114. (unless (equal url (eww-current-url))
  115. (eww-mode)
  116. (eww (if (consp url) (car url) url))))
  117. (advice-add 'eww-open-in-new-buffer :override 'ambrevar/eww-open-in-new-buffer)
  118. (defun ambrevar/eww-name-buffer-with-title ()
  119. "Include the page title in current EWW buffer name."
  120. (interactive)
  121. (when (derived-mode-p 'eww-mode)
  122. (rename-buffer (format "*eww: %s*" (plist-get eww-data :title)) t)))
  123. (defun ambrevar/eww-update-header-line-format ()
  124. (setq header-line-format
  125. (and eww-header-line-format
  126. (let ((title (plist-get eww-data :title))
  127. (peer (plist-get eww-data :peer)))
  128. (when (zerop (length title))
  129. (setq title "[untitled]"))
  130. ;; This connection has is https.
  131. (when peer
  132. (setq title
  133. (propertize title 'face
  134. (if (plist-get peer :warnings)
  135. 'eww-invalid-certificate
  136. 'eww-valid-certificate))))
  137. (replace-regexp-in-string
  138. "%" "%%"
  139. (format-spec
  140. eww-header-line-format
  141. `((?u . ,(or (plist-get eww-data :url) ""))
  142. (?t . ,title)))))))
  143. ;; PATCH
  144. (ambrevar/eww-name-buffer-with-title))
  145. (advice-add 'eww-update-header-line-format :override 'ambrevar/eww-update-header-line-format)
  146. ;; TODO: Fix quickmarks bindings. Or maybe just display the quickmarks buffer
  147. ;; and start `eww', which follows the quickmarks when first word is the mark.
  148. ;; TODO: Merge qutebrowser quickmarks.
  149. ;; TODO: Add bookmark editing functions such as edit title, tags, quickmark,
  150. ;; search-engine. Use eww-buffers and Helm.
  151. (defun ambrevar/eww-add-bookmark (&optional url title)
  152. "Bookmark the current page.
  153. With prefix argument, prompt for bookmark title."
  154. (setq url (or url (plist-get eww-data :url)))
  155. (setq title (or title (plist-get eww-data :title)))
  156. (interactive)
  157. (eww-read-bookmarks)
  158. (let (tag-list existing-bookmark)
  159. (dolist (bookmark eww-bookmarks)
  160. (when (equal
  161. ;; PATCH: Ignore protocol when sorting.
  162. ;; TODO: Include "sort-bookmarks": Warn for unique tags, warn for same URL up to paragraph. Make this customizable.
  163. ;; TODO: Smarter: Store bookmarks as an alist/hash-table indexed by domain.tld.
  164. (replace-regexp-in-string "^https?" "" url)
  165. (replace-regexp-in-string "^https?" "" (plist-get bookmark :url)))
  166. (if existing-bookmark
  167. (user-error "Duplicate bookmarks: %s, %s" existing-bookmark bookmark)
  168. (setq existing-bookmark bookmark)))
  169. (setq tag-list (append tag-list (plist-get bookmark :tags))))
  170. (cl-delete-duplicates tag-list)
  171. (let ((tags (completing-read-multiple (format "%s for bookmark (comma separated): "
  172. (if existing-bookmark "Update tags" "Tags"))
  173. tag-list
  174. nil nil nil nil
  175. (and existing-bookmark
  176. (mapconcat 'identity
  177. (plist-get existing-bookmark :tags) ","))))
  178. (existing-title (or (and existing-bookmark (plist-get existing-bookmark :title))
  179. ""))
  180. (sanitize-title (lambda (title)
  181. (setq title (string-trim title))
  182. (setq title (replace-regexp-in-string "[\n\t\r]" " " title)))))
  183. (setq title (funcall sanitize-title title))
  184. (setq existing-title (funcall sanitize-title existing-title))
  185. (setq title (or (and (not current-prefix-arg)
  186. (not (string-empty-p (if existing-bookmark existing-title title)))
  187. (if existing-bookmark existing-title title))
  188. (completing-read "Title: "
  189. (if existing-bookmark
  190. (list title existing-title)
  191. (list title))
  192. nil nil nil nil
  193. (if existing-bookmark existing-title title))))
  194. (setq title (funcall sanitize-title title))
  195. (let ((new-bookmark `(:url ,url
  196. :title ,title
  197. :time ,(or (and existing-bookmark (plist-get existing-bookmark :time))
  198. (current-time-string))
  199. ,@(if tags (list :tags tags)))))
  200. (if existing-bookmark
  201. (cl-nsubstitute new-bookmark existing-bookmark eww-bookmarks :count 1)
  202. (push new-bookmark eww-bookmarks)))
  203. (eww-write-bookmarks)
  204. (message "Bookmarked %s (%s)" url title))))
  205. (advice-add 'eww-add-bookmark :override 'ambrevar/eww-add-bookmark)
  206. (defvar ambrevar/eww-bookmark-timestamp nil)
  207. (defun ambrevar/eww-read-bookmarks ()
  208. (let* ((file ambrevar/eww-bookmarks-file)
  209. (mtime (file-attribute-modification-time (file-attributes file))))
  210. (when (or (null ambrevar/eww-bookmark-timestamp)
  211. (null eww-bookmarks)
  212. (time-less-p ambrevar/eww-bookmark-timestamp
  213. mtime))
  214. (setq ambrevar/eww-bookmark-timestamp mtime)
  215. (setq eww-bookmarks
  216. (unless (zerop (or (file-attribute-size (file-attributes file)) 0))
  217. (with-temp-buffer
  218. (insert-file-contents file)
  219. (read (current-buffer))))))))
  220. (advice-add 'eww-read-bookmarks :override 'ambrevar/eww-read-bookmarks)
  221. (defun ambrevar/eww-write-bookmarks ()
  222. (require 'rx)
  223. ;; PATCH
  224. (setq eww-bookmarks
  225. (sort eww-bookmarks
  226. (lambda (a b) (string<
  227. ;; Ignore protocol when sorting.
  228. (replace-regexp-in-string "^[a-zA-Z]+://" "" (plist-get a :url))
  229. (replace-regexp-in-string "^[a-zA-Z]+://" "" (plist-get b :url))))))
  230. ;; PATCH: Remove newline for tags.
  231. (let* ((visiting (find-buffer-visiting ambrevar/eww-bookmarks-file))
  232. (buffer (or visiting (find-file-noselect ambrevar/eww-bookmarks-file))))
  233. (with-current-buffer buffer
  234. (erase-buffer)
  235. (insert
  236. (replace-regexp-in-string
  237. (rx ":tags" ?\n (1+ space)) ":tags "
  238. (with-temp-buffer
  239. (insert ";; Auto-generated file; don't edit -*- mode: emacs-lisp; -*-\n")
  240. (pp eww-bookmarks (current-buffer))
  241. (buffer-string))))
  242. (save-buffer))
  243. (unless visiting (kill-buffer buffer))
  244. (setq ambrevar/eww-bookmark-timestamp
  245. (file-attribute-modification-time ambrevar/eww-bookmark-timestamp))))
  246. (advice-add 'eww-write-bookmarks :override 'ambrevar/eww-write-bookmarks)
  247. (defvar ambrevar/eww-quickmark-prefix ""
  248. "Prefix to load a quickmark.")
  249. (defun ambrevar/eww-bookmark-prepare ()
  250. ;; PATCH: Don't load if already loaded. This allows for overrides (e.g. quickmarks).
  251. (unless eww-bookmarks
  252. (eww-read-bookmarks))
  253. (unless eww-bookmarks
  254. (user-error "No bookmarks are defined"))
  255. (set-buffer (get-buffer-create "*eww bookmarks*"))
  256. (eww-bookmark-mode)
  257. (let* ((width (/ (window-width) 2))
  258. (format (format "%%-%ds %%s" width))
  259. (inhibit-read-only t)
  260. start title)
  261. (erase-buffer)
  262. (setq header-line-format (concat " " (format format "Title" "URL")))
  263. (dolist (bookmark eww-bookmarks)
  264. (setq start (point)
  265. title (plist-get bookmark :title))
  266. ;; PATCH: Show quickmark.
  267. (when (plist-get bookmark :mark)
  268. (setq title (format "[%s] %s" (plist-get bookmark :mark) title)))
  269. (when (> (length title) width)
  270. (setq title (truncate-string-to-width title width)))
  271. (insert (format format title
  272. (concat (plist-get bookmark :url)
  273. (when (plist-get bookmark :tags)
  274. (concat " ("
  275. (mapconcat 'identity (plist-get bookmark :tags) ",")
  276. ")"))))
  277. "\n")
  278. (put-text-property start (1+ start) 'eww-bookmark bookmark)
  279. ;; PATCH: Bind keys
  280. (when (plist-get bookmark :mark)
  281. (define-key eww-bookmark-mode-map
  282. (kbd (concat ambrevar/eww-quickmark-prefix (plist-get bookmark :mark)))
  283. (lambda (&optional new-window)
  284. (interactive "P")
  285. (if new-window
  286. (ambrevar/eww-open-in-new-buffer (plist-get bookmark :url))
  287. (eww (plist-get bookmark :url)))))))
  288. (goto-char (point-min))))
  289. (advice-add 'eww-bookmark-prepare :override 'ambrevar/eww-bookmark-prepare)
  290. (defun ambrevar/eww-quickmarks (&optional new-window)
  291. "Display quickmarks."
  292. (interactive "P")
  293. (let ((eww-bookmarks (seq-filter (lambda (b) (plist-get b :mark)) eww-bookmarks)))
  294. (eww-list-bookmarks)))
  295. (defun ambrevar/eww-bookmarks-list-by-tags (&optional arg)
  296. "Return bookmarks matching one of the specified tags.
  297. With prefix argument or ARG, bookmarks much match all tags."
  298. (let ((tag-list (delq nil (mapcar (lambda (b) (plist-get b :tags)) eww-bookmarks))))
  299. (seq-uniq (mapcar 'append tag-list))
  300. (let ((tags (completing-read-multiple "Tags for bookmark (comma separated): " tag-list)))
  301. (seq-filter (lambda (b)
  302. (if arg
  303. (null (seq-difference tags (plist-get b :tags)))
  304. (seq-intersection tags (plist-get b :tags))))
  305. eww-bookmarks))))
  306. (defun ambrevar/eww-bookmarks-by-tags (&optional arg)
  307. "Display bookmarks matching one of the specified tags.
  308. With prefix argument or ARG, bookmarks much match all tags."
  309. (interactive "P")
  310. (let ((eww-bookmarks (ambrevar/eww-bookmarks-list-by-tags arg)))
  311. (eww-list-bookmarks)))
  312. (defun ambrevar/eww--dwim-expand-url (url)
  313. (setq url (string-trim url))
  314. (cond ((string-match-p "\\`file:/" url))
  315. ;; Don't mangle file: URLs at all.
  316. ((string-match-p "\\`ftp://" url)
  317. (user-error "FTP is not supported"))
  318. (t
  319. ;; Anything that starts with something that vaguely looks
  320. ;; like a protocol designator is interpreted as a full URL.
  321. (if (or (string-match "\\`[A-Za-z]+:" url)
  322. ;; Also try to match "naked" URLs like
  323. ;; en.wikipedia.org/wiki/Free software
  324. (string-match "\\`[A-Za-z_]+\\.[A-Za-z._]+/" url)
  325. (and (= (length (split-string url)) 1)
  326. (or (and (not (string-match-p "\\`[\"'].*[\"']\\'" url))
  327. (> (length (split-string url "[.:]")) 1))
  328. (string-match eww-local-regex url))))
  329. (progn
  330. (unless (string-match-p "\\`[a-zA-Z][-a-zA-Z0-9+.]*://" url)
  331. (setq url (concat "http://" url)))
  332. ;; Some sites do not redirect final /
  333. (when (string= (url-filename (url-generic-parse-url url)) "")
  334. (setq url (concat url "/"))))
  335. ;; PATCH: Add support for search engines and quickmarks.
  336. (string-match (rx (group (1+ (not space)))
  337. (0+ space)
  338. (? (group (0+ any))))
  339. url)
  340. (let* ((first-word (match-string 1 url))
  341. (rest-url (match-string 2 url))
  342. (marks (make-hash-table :test 'equal))
  343. (engines (make-hash-table :test 'equal)))
  344. (dolist (b eww-bookmarks)
  345. (let ((mark (plist-get b :mark))
  346. engine)
  347. (when mark
  348. (puthash mark (plist-get b :url) marks)
  349. (setq engine (plist-get b :search))
  350. (when engine
  351. (puthash mark (concat (let ((case-fold-search t))
  352. (unless (string-match "^https?://" engine)
  353. (plist-get b :url)))
  354. engine)
  355. engines)))))
  356. (cond
  357. ((and (gethash first-word engines)
  358. (not (string= rest-url "")) )
  359. (setq url (format (gethash first-word engines) ; Engines must have exactly one "%s".
  360. (mapconcat
  361. #'url-hexify-string (split-string rest-url) "+"))))
  362. ((and (gethash first-word marks)
  363. (string= rest-url ""))
  364. (setq url (gethash first-word marks)))
  365. (t (setq url (concat eww-search-prefix
  366. (mapconcat
  367. #'url-hexify-string (split-string url) "+")))))))))
  368. url)
  369. (advice-add 'eww--dwim-expand-url :override 'ambrevar/eww--dwim-expand-url)
  370. (provide 'init-eww)