2504 lines
97 KiB
EmacsLisp
2504 lines
97 KiB
EmacsLisp
;;; eaf.el --- Emacs application framework -*- lexical-binding: t; -*-
|
||
|
||
;; Filename: eaf.el
|
||
;; Description: Emacs application framework
|
||
;; Author: Andy Stewart <lazycat.manatee@gmail.com>
|
||
;; Maintainer: Andy Stewart <lazycat.manatee@gmail.com>
|
||
;; Copyright (C) 2018, Andy Stewart, all rights reserved.
|
||
;; Created: 2018-06-15 14:10:12
|
||
;; Version: 0.5
|
||
;; Last-Updated: Wed Jan 27 09:25:35 2021 (-0500)
|
||
;; By: Mingde (Matthew) Zeng
|
||
;; URL: https://github.com/manateelazycat/emacs-application-framework
|
||
;; Keywords:
|
||
;; Compatibility: emacs-version >= 27
|
||
;;
|
||
;; Features that might be required by this library:
|
||
;;
|
||
;; Please check README
|
||
;;
|
||
|
||
;;; This file is NOT part of GNU Emacs
|
||
|
||
;;; License
|
||
;;
|
||
;; This program is free software; you can redistribute it and/or modify
|
||
;; it under the terms of the GNU General Public License as published by
|
||
;; the Free Software Foundation; either version 3, or (at your option)
|
||
;; any later version.
|
||
|
||
;; This program is distributed in the hope that it will be useful,
|
||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
;; GNU General Public License for more details.
|
||
|
||
;; You should have received a copy of the GNU General Public License
|
||
;; along with this program; see the file COPYING. If not, write to
|
||
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
|
||
;; Floor, Boston, MA 02110-1301, USA.
|
||
|
||
;;; Commentary:
|
||
;;
|
||
;; Emacs Application Framework
|
||
;;
|
||
|
||
;;; Installation:
|
||
;;
|
||
;; Please check README
|
||
;;
|
||
|
||
;;; Customize:
|
||
;;
|
||
;;
|
||
;;
|
||
;; All of the above can customize by:
|
||
;; M-x customize-group RET eaf RET
|
||
;;
|
||
|
||
;;; Change log:
|
||
;;
|
||
;; 2018/06/15
|
||
;; * First released.
|
||
;;
|
||
|
||
;;; Acknowledgements:
|
||
;;
|
||
;;
|
||
;;
|
||
|
||
;;; TODO
|
||
;;
|
||
;;
|
||
;;
|
||
|
||
;;; Code:
|
||
|
||
(defun add-subdirs-to-load-path (dir)
|
||
"Recursive add directory DIR to `load-path'."
|
||
(mapcar
|
||
(lambda (path) (add-to-list 'load-path path))
|
||
(delete-dups (mapcar 'file-name-directory (directory-files-recursively dir "\.el$")))))
|
||
|
||
(add-subdirs-to-load-path (expand-file-name "app" (file-name-directory (locate-library "eaf"))))
|
||
|
||
;;;###autoload
|
||
(defun eaf-install-dependencies ()
|
||
"An interactive function that run install-eaf.sh or install-eaf-win32.js for Linux or Windows respectively."
|
||
(interactive)
|
||
(let ((eaf-dir (file-name-directory (locate-library "eaf"))))
|
||
(cond ((eq system-type 'gnu/linux)
|
||
(let ((default-directory "/sudo::"))
|
||
(shell-command (concat eaf-dir "install-eaf.sh" "&"))))
|
||
((memq system-type '(cygwin windows-nt ms-dos))
|
||
(shell-command (format "node %s" (concat eaf-dir "install-eaf-win32.js" "&"))))
|
||
((eq system-type 'darwin)
|
||
(user-error "Unfortunately MacOS is not supported, see README for details")))))
|
||
|
||
(require 'subr-x)
|
||
(require 'map)
|
||
(require 'bookmark)
|
||
(require 'seq)
|
||
(require 'eaf-mindmap)
|
||
(require 'eaf-interleave)
|
||
(require 'json)
|
||
(require 's)
|
||
(require 'eaf-server)
|
||
(require 'epc)
|
||
|
||
;; Remove the relevant environment variables from the process-environment to disable QT scaling,
|
||
;; let EAF qt program follow the system scale.
|
||
(setq process-environment (seq-filter
|
||
(lambda(var)
|
||
(and (not (string-match-p "QT_SCALE_FACTOR" var))
|
||
(not (string-match-p "QT_SCREEN_SCALE_FACTOR" var)))) process-environment))
|
||
|
||
(defgroup eaf nil
|
||
"Emacs Application Framework."
|
||
:group 'applications)
|
||
|
||
(defcustom eaf-mode-hook '()
|
||
"EAF mode hook."
|
||
:type 'hook)
|
||
|
||
(defvar eaf-mode-map*
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map (kbd "C-h m") #'eaf-describe-bindings)
|
||
(define-key map [remap describe-bindings] #'eaf-describe-bindings)
|
||
(define-key map (kbd "C-c b") #'eaf-open-bookmark)
|
||
(define-key map (kbd "C-c i") #'eaf-import-chrome-bookmarks)
|
||
(define-key map (kbd "C-c e") #'eaf-open-external)
|
||
(define-key map (kbd "M-'") #'eaf-toggle-fullscreen)
|
||
(define-key map (kbd "M-/") #'eaf-get-path-or-url)
|
||
(define-key map (kbd "M-[") #'eaf-share-path-or-url)
|
||
(define-key map (vector 'remap #'keyboard-quit) #'eaf-keyboard-quit)
|
||
(define-key map (vector 'remap #'self-insert-command) #'eaf-send-key)
|
||
(dolist (single-key '("RET" "DEL" "TAB" "SPC" "<backtab>" "<home>" "<end>" "<left>" "<right>" "<up>" "<down>" "<prior>" "<next>" "<delete>" "<backspace>" "<return>"))
|
||
(define-key map (kbd single-key) #'eaf-send-key))
|
||
map)
|
||
"Keymap for default bindings available in all apps.")
|
||
|
||
(defvar eaf-mode-map nil
|
||
"Keymap used by `eaf-mode'.
|
||
|
||
Don't modify this map directly. To bind keys for all apps use
|
||
`eaf-mode-map*' and to bind keys for individual apps use
|
||
`eaf-bind-key'.")
|
||
|
||
(defvar eaf-edit-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map (kbd "C-c C-t") #'eaf-edit-buffer-switch-to-org-mode)
|
||
(define-key map (kbd "C-c C-k") #'eaf-edit-buffer-cancel)
|
||
(define-key map (kbd "C-c C-c") #'eaf-edit-buffer-confirm)
|
||
map))
|
||
|
||
(define-derived-mode eaf-edit-mode text-mode "EAF/edit"
|
||
"The major mode to edit focus text input.")
|
||
|
||
(defun eaf-describe-bindings ()
|
||
"Like `describe-bindings' for EAF buffers."
|
||
(interactive)
|
||
(let ((emulation-mode-map-alists nil)
|
||
(eaf-mode-map (current-local-map)))
|
||
(call-interactively 'describe-mode)))
|
||
|
||
(defvar-local eaf--buffer-id nil
|
||
"Internal id used by EAF app.")
|
||
|
||
(defvar-local eaf--buffer-url nil
|
||
"EAF buffer-local URL.")
|
||
|
||
(defvar-local eaf--buffer-app-name nil
|
||
"EAF buffer-local app-name.")
|
||
|
||
(defvar-local eaf--buffer-args nil
|
||
"EAF buffer-local args.")
|
||
|
||
(defvar-local eaf--buffer-map-alist nil
|
||
"EAF buffer-local map alist.")
|
||
|
||
(defvar-local eaf--buffer-map-alist-order 1
|
||
"Order of EAF buffer-local map alist in `emulation-mode-map-alists'.")
|
||
|
||
(define-derived-mode eaf-mode fundamental-mode "EAF"
|
||
"Major mode for Emacs Application Framework buffers.
|
||
|
||
This mode is used by all apps. The mode map `eaf-mode-map' is
|
||
created dynamically for each app and should not be changed
|
||
manually. See `eaf-bind-key' for customization of app bindings.
|
||
|
||
Within EAF buffers the variable `eaf--buffer-app-name' holds the
|
||
name of the current app. Each app can setup app hooks by using
|
||
`eaf-<app-name>-hook'. This hook runs after the app buffer has
|
||
been initialized."
|
||
;; Split window combinations proportionally.
|
||
(setq-local window-combination-resize t)
|
||
;; Disable cursor in eaf buffer.
|
||
(setq-local cursor-type nil)
|
||
|
||
(set (make-local-variable 'eaf--buffer-id) (eaf--generate-id))
|
||
(setq-local bookmark-make-record-function #'eaf--bookmark-make-record)
|
||
|
||
;; Copy default value in case user already has bindings there
|
||
(setq-local emulation-mode-map-alists
|
||
(copy-alist (default-value 'emulation-mode-map-alists)))
|
||
;; Construct map alist
|
||
(setq-local eaf--buffer-map-alist (list (cons t eaf-mode-map)))
|
||
;; Eanble mode map and make it the first priority
|
||
(add-to-ordered-list
|
||
'emulation-mode-map-alists
|
||
'eaf--buffer-map-alist
|
||
'eaf--buffer-map-alist-order)
|
||
|
||
(add-hook 'kill-buffer-hook #'eaf--monitor-buffer-kill nil t)
|
||
(add-hook 'kill-emacs-hook #'eaf--monitor-emacs-kill))
|
||
|
||
(defvar eaf-python-file (expand-file-name "eaf.py" (file-name-directory load-file-name)))
|
||
|
||
(defvar eaf-server-port 9999)
|
||
|
||
(defvar eaf-epc-process nil)
|
||
|
||
(defvar eaf-internal-process nil)
|
||
(defvar eaf-internal-process-prog nil)
|
||
(defvar eaf-internal-process-args nil)
|
||
|
||
(defvar eaf--active-buffers nil
|
||
"Contains a list of '(buffer-url buffer-app-name buffer-args).")
|
||
|
||
(defvar eaf--webengine-include-private-codec nil)
|
||
|
||
(defvar eaf-org-file-list '())
|
||
|
||
(defvar eaf-org-killed-file-list '())
|
||
|
||
(defvar eaf-last-frame-width 0)
|
||
|
||
(defvar eaf-last-frame-height 0)
|
||
|
||
(defcustom eaf-name "*eaf*"
|
||
"Name of EAF buffer."
|
||
:type 'string)
|
||
|
||
(defcustom eaf-browser-search-engines `(("google" . "http://www.google.com/search?ie=utf-8&oe=utf-8&q=%s")
|
||
("duckduckgo" . "https://duckduckgo.com/?q=%s"))
|
||
"The default search engines offered by EAF.
|
||
|
||
Each element has the form (NAME . URL).
|
||
NAME is a search engine name, as a string.
|
||
URL pecifies the url format for the search engine.
|
||
It should have a %s as placeholder for search string."
|
||
:type '(alist :key-type (string :tag "Search engine name")
|
||
:value-type (string :tag "Search engine url")))
|
||
|
||
(defcustom eaf-browser-default-search-engine "google"
|
||
"The default search engine used by `eaf-open-browser' and `eaf-search-it'.
|
||
|
||
It must defined at `eaf-browser-search-engines'."
|
||
:type 'string)
|
||
|
||
(defcustom eaf-python-command (if (memq system-type '(cygwin windows-nt ms-dos)) "python3.exe" "python3")
|
||
"The Python interpreter used to run eaf.py."
|
||
:type 'string)
|
||
|
||
(defcustom eaf-config-location (expand-file-name (locate-user-emacs-file "eaf/"))
|
||
"Directory where eaf will store configuration files."
|
||
:type 'directory)
|
||
|
||
(defcustom eaf-chrome-bookmark-file "~/.config/google-chrome/Default/Bookmarks"
|
||
"The default chrome bookmark file to import."
|
||
:type 'string)
|
||
|
||
(defcustom eaf-var-list
|
||
'((eaf-camera-save-path . "~/Downloads")
|
||
(eaf-browser-enable-plugin . "true")
|
||
(eaf-browser-enable-adblocker . "false")
|
||
(eaf-browser-enable-autofill . "false")
|
||
(eaf-browser-enable-javascript . "true")
|
||
(eaf-browser-remember-history . "true")
|
||
(eaf-browser-default-zoom . "1.0")
|
||
(eaf-browser-font-family . "")
|
||
(eaf-browser-blank-page-url . "https://www.google.com")
|
||
(eaf-browser-scroll-behavior . "auto")
|
||
(eaf-browser-download-path . "~/Downloads")
|
||
(eaf-browser-aria2-proxy-host . "")
|
||
(eaf-browser-aria2-proxy-port . "")
|
||
(eaf-browser-dark-mode . "follow")
|
||
(eaf-browser-chrome-history-file . "~/.config/google-chrome/Default/History")
|
||
;; DisallowUnknownUrlSchemes, AllowUnknownUrlSchemesFromUserInteraction, or AllowAllUnknownUrlSchemes
|
||
(eaf-browser-unknown-url-scheme-policy . "AllowUnknownUrlSchemesFromUserInteraction")
|
||
(eaf-pdf-dark-mode . "follow")
|
||
(eaf-pdf-default-zoom . "1.0")
|
||
(eaf-pdf-dark-exclude-image . "true")
|
||
(eaf-pdf-scroll-ratio . "0.05")
|
||
(eaf-terminal-dark-mode . "follow")
|
||
(eaf-terminal-font-size . "13")
|
||
(eaf-terminal-font-family . "")
|
||
(eaf-markdown-dark-mode . "follow")
|
||
(eaf-mindmap-dark-mode . "follow")
|
||
(eaf-mindmap-save-path . "~/Documents")
|
||
(eaf-mindmap-edit-mode . "false")
|
||
(eaf-jupyter-font-size . "13")
|
||
(eaf-jupyter-font-family . "")
|
||
(eaf-jupyter-dark-mode . "follow")
|
||
(eaf-marker-letters . "ASDFHJKLWEOPCNM")
|
||
(eaf-emacs-theme-mode . "")
|
||
(eaf-emacs-theme-background-color . "")
|
||
(eaf-emacs-theme-foreground-color . ""))
|
||
"The alist storing user-defined variables that's shared with EAF Python side.
|
||
|
||
Try not to modify this alist directly. Use `eaf-setq' to modify instead."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-browser-caret-mode-keybinding
|
||
'(("j" . "caret_next_line")
|
||
("k" . "caret_previous_line")
|
||
("l" . "caret_next_character")
|
||
("h" . "caret_previous_character")
|
||
("w" . "caret_next_word")
|
||
("b" . "caret_previous_word")
|
||
(")" . "caret_next_sentence")
|
||
("(" . "caret_previous_sentence")
|
||
("g" . "caret_to_bottom")
|
||
("G" . "caret_to_top")
|
||
("/" . "caret_search_forward")
|
||
("?" . "caret_search_backward")
|
||
("." . "caret_clear_search")
|
||
("v" . "caret_toggle_mark")
|
||
("o" . "caret_rotate_selection")
|
||
("y" . "caret_translate_text")
|
||
("q" . "caret_exit")
|
||
("C-n" . "caret_next_line")
|
||
("C-p" . "caret_previous_line")
|
||
("C-f" . "caret_next_character")
|
||
("C-b" . "caret_previous_character")
|
||
("M-f" . "caret_next_word")
|
||
("M-b" . "caret_previous_word")
|
||
("M-e" . "caret_next_sentence")
|
||
("M-a" . "caret_previous_sentence")
|
||
("C-<" . "caret_to_bottom")
|
||
("C->" . "caret_to_top")
|
||
("C-s" . "caret_search_forward")
|
||
("C-r" . "caret_search_backward")
|
||
("C-." . "caret_clear_search")
|
||
("C-SPC" . "caret_toggle_mark")
|
||
("C-o" . "caret_rotate_selection")
|
||
("C-y" . "caret_translate_text")
|
||
("C-q" . "caret_exit")
|
||
("c" . "insert_or_caret_at_line")
|
||
("M-c" . "caret_toggle_browsing")
|
||
("<escape>" . "caret_exit"))
|
||
"The keybinding of EAF Browser Caret Mode."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-browser-keybinding
|
||
'(("C--" . "zoom_out")
|
||
("C-=" . "zoom_in")
|
||
("C-0" . "zoom_reset")
|
||
("C-s" . "search_text_forward")
|
||
("C-r" . "search_text_backward")
|
||
("C-n" . "scroll_up")
|
||
("C-p" . "scroll_down")
|
||
("C-f" . "scroll_right")
|
||
("C-b" . "scroll_left")
|
||
("C-v" . "scroll_up_page")
|
||
("C-y" . "yank_text")
|
||
("C-w" . "kill_text")
|
||
("M-e" . "atomic_edit")
|
||
("M-c" . "caret_toggle_browsing")
|
||
("M-D" . "select_text")
|
||
("M-s" . "open_link")
|
||
("M-S" . "open_link_new_buffer")
|
||
("M-B" . "open_link_background_buffer")
|
||
("C-/" . "undo_action")
|
||
("M-_" . "redo_action")
|
||
("M-w" . "copy_text")
|
||
("M-f" . "history_forward")
|
||
("M-b" . "history_backward")
|
||
("M-q" . "clear_cookies")
|
||
("C-t" . "toggle_password_autofill")
|
||
("C-d" . "save_page_password")
|
||
("M-a" . "toggle_adblocker")
|
||
("C-M-q" . "clear_history")
|
||
("C-M-i" . "import_chrome_history")
|
||
("M-v" . "scroll_down_page")
|
||
("M-<" . "scroll_to_begin")
|
||
("M->" . "scroll_to_bottom")
|
||
("M-p" . "duplicate_page")
|
||
("M-t" . "new_blank_page")
|
||
("M-d" . "toggle_dark_mode")
|
||
("SPC" . "insert_or_scroll_up_page")
|
||
("J" . "insert_or_select_left_tab")
|
||
("K" . "insert_or_select_right_tab")
|
||
("j" . "insert_or_scroll_up")
|
||
("k" . "insert_or_scroll_down")
|
||
("h" . "insert_or_scroll_left")
|
||
("l" . "insert_or_scroll_right")
|
||
("f" . "insert_or_open_link")
|
||
("F" . "insert_or_open_link_new_buffer")
|
||
("B" . "insert_or_open_link_background_buffer")
|
||
("c" . "insert_or_caret_at_line")
|
||
("u" . "insert_or_scroll_down_page")
|
||
("d" . "insert_or_scroll_up_page")
|
||
("H" . "insert_or_history_backward")
|
||
("L" . "insert_or_history_forward")
|
||
("t" . "insert_or_new_blank_page")
|
||
("T" . "insert_or_recover_prev_close_page")
|
||
("i" . "insert_or_focus_input")
|
||
("I" . "insert_or_open_downloads_setting")
|
||
("r" . "insert_or_refresh_page")
|
||
("g" . "insert_or_scroll_to_begin")
|
||
("x" . "insert_or_close_buffer")
|
||
("G" . "insert_or_scroll_to_bottom")
|
||
("-" . "insert_or_zoom_out")
|
||
("=" . "insert_or_zoom_in")
|
||
("0" . "insert_or_zoom_reset")
|
||
("m" . "insert_or_save_as_bookmark")
|
||
("o" . "insert_or_open_browser")
|
||
("y" . "insert_or_download_youtube_video")
|
||
("Y" . "insert_or_download_youtube_audio")
|
||
("p" . "insert_or_toggle_device")
|
||
("P" . "insert_or_duplicate_page")
|
||
("1" . "insert_or_save_as_pdf")
|
||
("2" . "insert_or_save_as_single_file")
|
||
("v" . "insert_or_view_source")
|
||
("e" . "insert_or_edit_url")
|
||
("M-C" . "copy_code")
|
||
("C-M-f" . "copy_link")
|
||
("C-a" . "select_all_or_input_text")
|
||
("M-u" . "clear_focus")
|
||
("C-j" . "open_downloads_setting")
|
||
("M-o" . "eval_js")
|
||
("M-O" . "eval_js_file")
|
||
("<escape>" . "eaf-browser-send-esc-or-exit-fullscreen")
|
||
("M-," . "eaf-send-down-key")
|
||
("M-." . "eaf-send-up-key")
|
||
("M-m" . "eaf-send-return-key")
|
||
("<f5>" . "refresh_page")
|
||
("<f12>" . "open_devtools")
|
||
("<C-return>" . "eaf-send-ctrl-return-sequence")
|
||
)
|
||
"The keybinding of EAF Browser."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-browser-key-alias
|
||
'(("C-a" . "<home>")
|
||
("C-e" . "<end>"))
|
||
"The key alias of EAF Browser."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-pdf-viewer-keybinding
|
||
'(("j" . "scroll_up")
|
||
("<down>" . "scroll_up")
|
||
("C-n" . "scroll_up")
|
||
("k" . "scroll_down")
|
||
("<up>" . "scroll_down")
|
||
("C-p" . "scroll_down")
|
||
("h" . "scroll_left")
|
||
("<left>" . "scroll_left")
|
||
("C-b" . "scroll_left")
|
||
("l" . "scroll_right")
|
||
("<right>" . "scroll_right")
|
||
("C-f" . "scroll_right")
|
||
("SPC" . "scroll_up_page")
|
||
("b" . "scroll_down_page")
|
||
("C-v" . "scroll_up_page")
|
||
("M-v" . "scroll_down_page")
|
||
("t" . "toggle_read_mode")
|
||
("0" . "zoom_reset")
|
||
("=" . "zoom_in")
|
||
("-" . "zoom_out")
|
||
("g" . "scroll_to_begin")
|
||
("G" . "scroll_to_end")
|
||
("p" . "jump_to_page")
|
||
("P" . "jump_to_percent")
|
||
("[" . "save_current_pos")
|
||
("]" . "jump_to_saved_pos")
|
||
("i" . "toggle_inverted_mode")
|
||
("m" . "toggle_mark_link")
|
||
("f" . "jump_to_link")
|
||
("M-w" . "copy_select")
|
||
("C-s" . "search_text_forward")
|
||
("C-r" . "search_text_backward")
|
||
("x" . "close_buffer")
|
||
("C-<right>" . "rotate_clockwise")
|
||
("C-<left>" . "rotate_counterclockwise")
|
||
("M-h" . "add_annot_highlight")
|
||
("M-u" . "add_annot_underline")
|
||
("M-s" . "add_annot_squiggly")
|
||
("M-d" . "add_annot_strikeout_or_delete_annot")
|
||
("M-e" . "add_annot_text_or_edit_annot")
|
||
("M-p" . "toggle_presentation_mode")
|
||
("J" . "select_left_tab")
|
||
("K" . "select_right_tab")
|
||
("o" . "eaf-pdf-outline"))
|
||
"The keybinding of EAF PDF Viewer."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-video-player-keybinding
|
||
'(("SPC" . "toggle_play")
|
||
("x" . "close_buffer")
|
||
("h" . "play_backward")
|
||
("l" . "play_forward"))
|
||
"The keybinding of EAF Video Player."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-js-video-player-keybinding
|
||
'(("SPC" . "toggle_play")
|
||
("M-g" . "exit_fullscreen")
|
||
("<f12>" . "open_devtools")
|
||
("h" . "backward")
|
||
("l" . "forward")
|
||
("r" . "restart")
|
||
("j" . "decrease_volume")
|
||
("k" . "increase_volume")
|
||
("x" . "close_buffer")
|
||
("c--" . "zoom_out")
|
||
("C-=" . "zoom_in")
|
||
("C-0" . "zoom_reset")
|
||
)
|
||
"The keybinding of EAF JS Video Player."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-image-viewer-keybinding
|
||
'(("n" . "load_next_image")
|
||
("p" . "load_prev_image")
|
||
("SPC" . "load_prev_image")
|
||
("-" . "zoom_out")
|
||
("=" . "zoom_in")
|
||
("0" . "zoom_reset")
|
||
("9" . "zoom_toggle")
|
||
("x" . "close_buffer")
|
||
("u" . "rotate_left")
|
||
("i" . "rotate_right")
|
||
("y" . "flip_horizontal")
|
||
("o" . "flip_vertical")
|
||
("<f12>" . "open_devtools")
|
||
)
|
||
"The keybinding of EAF Image Viewer."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-terminal-keybinding
|
||
'(("M-n" . "scroll_up")
|
||
("M-p" . "scroll_down")
|
||
("C-v" . "scroll_up_page")
|
||
("M-v" . "scroll_down_page")
|
||
("M-<" . "scroll_to_begin")
|
||
("M->" . "scroll_to_bottom")
|
||
("C--" . "zoom_out")
|
||
("C-=" . "zoom_in")
|
||
("C-0" . "zoom_reset")
|
||
("C-S-c" . "copy_text")
|
||
("C-S-v" . "yank_text")
|
||
("C-s" . "search_text_forward")
|
||
("M-s" . "search_text_backward")
|
||
("C-a" . "eaf-send-key-sequence")
|
||
("C-e" . "eaf-send-key-sequence")
|
||
("C-f" . "eaf-send-key-sequence")
|
||
("C-b" . "eaf-send-key-sequence")
|
||
("C-d" . "eaf-send-key-sequence")
|
||
("C-n" . "eaf-send-key-sequence")
|
||
("C-p" . "eaf-send-key-sequence")
|
||
("C-r" . "eaf-send-key-sequence")
|
||
("C-y" . "eaf-send-key-sequence")
|
||
("C-k" . "eaf-send-key-sequence")
|
||
("C-o" . "eaf-send-key-sequence")
|
||
("C-u" . "eaf-send-key-sequence")
|
||
("C-l" . "eaf-send-key-sequence")
|
||
("C-w" . "eaf-send-key-sequence")
|
||
("M-f" . "eaf-send-key-sequence")
|
||
("M-b" . "eaf-send-key-sequence")
|
||
("M-d" . "eaf-send-key-sequence")
|
||
("C-c C-c" . "eaf-send-second-key-sequence")
|
||
("C-c C-x" . "eaf-send-second-key-sequence")
|
||
("<f12>" . "open_devtools")
|
||
("M-w" . "copy_text")
|
||
("C-y" . "yank_text")
|
||
("C-S-a" . "select_all")
|
||
("C-S-l" . "clear_selection")
|
||
("C-M-l" . "clear")
|
||
("M-DEL" . "eaf-send-alt-backspace-sequence")
|
||
("M-<backspace>" . "eaf-send-alt-backspace-sequence"))
|
||
"The keybinding of EAF Terminal."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-camera-keybinding
|
||
'(("p" . "take_photo"))
|
||
"The keybinding of EAF Camera."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-mindmap-keybinding
|
||
'(("TAB" . "add_sub_node")
|
||
("RET" . "add_brother_node")
|
||
("<deletechar>" . "remove_node")
|
||
("M-m" . "update_node_topic")
|
||
("M-e" . "update_node_topic_inline")
|
||
("M-r" . "refresh_page")
|
||
("C--" . "zoom_out")
|
||
("C-=" . "zoom_in")
|
||
("C-0" . "zoom_reset")
|
||
("M-q" . "add_multiple_sub_nodes")
|
||
("M-RET" . "add_multiple_brother_nodes")
|
||
("M-i" . "add_multiple_middle_nodes")
|
||
("M-j" . "select_down_node")
|
||
("M-k" . "select_up_node")
|
||
("M-h" . "select_left_node")
|
||
("M-l" . "select_right_node")
|
||
("SPC" . "toggle_node_selection")
|
||
("C-n" . "eaf-send-down-key")
|
||
("C-p" . "eaf-send-up-key")
|
||
("C-f" . "eaf-send-right-key")
|
||
("C-b" . "eaf-send-left-key")
|
||
("x" . "insert_or_close_buffer")
|
||
("j" . "insert_or_select_down_node")
|
||
("k" . "insert_or_select_up_node")
|
||
("h" . "insert_or_select_left_node")
|
||
("l" . "insert_or_select_right_node")
|
||
("w" . "insert_or_copy_node_topic")
|
||
("y" . "insert_or_paste_node_topic")
|
||
("W" . "insert_or_cut_node_tree")
|
||
("Y" . "insert_or_paste_node_tree")
|
||
("J" . "insert_or_select_left_tab")
|
||
("K" . "insert_or_select_right_tab")
|
||
("-" . "insert_or_zoom_out")
|
||
("=" . "insert_or_zoom_in")
|
||
("0" . "insert_or_zoom_reset")
|
||
("d" . "insert_or_remove_node")
|
||
("D" . "insert_or_remove_middle_node")
|
||
("i" . "insert_or_add_middle_node")
|
||
("f" . "insert_or_update_node_topic")
|
||
("t" . "insert_or_toggle_node")
|
||
("b" . "insert_or_change_node_background")
|
||
("c" . "insert_or_change_background_color")
|
||
("C" . "insert_or_change_text_color")
|
||
("1" . "insert_or_save_screenshot")
|
||
("2" . "insert_or_save_file")
|
||
("3" . "insert_or_save_org_file")
|
||
("M-o" . "eval_js")
|
||
("M-p" . "eval_js_file")
|
||
("<f12>" . "open_devtools")
|
||
)
|
||
"The keybinding of EAF Mindmap."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-jupyter-keybinding
|
||
'(("C-+" . "zoom_in")
|
||
("C--" . "zoom_out")
|
||
("C-0" . "zoom_reset")
|
||
("C-l" . "eaf-send-key-sequence")
|
||
("C-a" . "eaf-send-key-sequence")
|
||
("C-e" . "eaf-send-key-sequence")
|
||
("C-u" . "eaf-send-key-sequence")
|
||
("C-k" . "eaf-send-key-sequence")
|
||
("C-y" . "eaf-send-key-sequence")
|
||
("C-p" . "eaf-send-key-sequence")
|
||
("C-n" . "eaf-send-key-sequence")
|
||
("C-f" . "eaf-send-key-sequence")
|
||
("C-b" . "eaf-send-key-sequence")
|
||
("C-d" . "eaf-send-key-sequence")
|
||
("M-b" . "eaf-send-key-sequence")
|
||
("M-f" . "eaf-send-key-sequence")
|
||
("M-d" . "eaf-send-key-sequence")
|
||
("M-<" . "eaf-send-key-sequence")
|
||
("M->" . "eaf-send-key-sequence")
|
||
("<C-return>" . "eaf-send-ctrl-return-sequence")
|
||
("<S-return>" . "eaf-send-shift-return-sequence")
|
||
)
|
||
"The keybinding of EAF Jupyter."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-pdf-extension-list
|
||
'("pdf" "xps" "oxps" "cbz" "epub" "fb2" "fbz" "djvu")
|
||
"The extension list of pdf application."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-markdown-extension-list
|
||
'("md")
|
||
"The extension list of markdown previewer application."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-image-extension-list
|
||
'("jpg" "jpeg" "png" "bmp" "gif" "svg" "webp")
|
||
"The extension list of image viewer application."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-video-extension-list
|
||
'("avi" "webm" "rmvb" "ogg" "mp4" "mkv" "m4v")
|
||
"The extension list of video player application."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-browser-extension-list
|
||
'("html" "htm")
|
||
"The extension list of browser application."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-org-extension-list
|
||
'("org")
|
||
"The extension list of org previewer application."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-mindmap-extension-list
|
||
'("emm")
|
||
"The extension list of mindmap application."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-office-extension-list
|
||
'("docx" "doc" "ppt" "pptx" "xlsx" "xls")
|
||
"The extension list of office application."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-find-file-ext-blacklist '()
|
||
"A blacklist of extensions to avoid when opening `find-file' file using EAF."
|
||
:type 'cons)
|
||
|
||
(defcustom eaf-mua-get-html
|
||
'(("^gnus-" . eaf-gnus-get-html)
|
||
("^mu4e-" . eaf-mu4e-get-html)
|
||
("^notmuch-" . eaf-notmuch-get-html))
|
||
"An alist regex mapping a MUA `major-mode' to a function to retrieve HTML part of a mail."
|
||
:type 'alist)
|
||
|
||
(defcustom eaf-browser-continue-where-left-off nil
|
||
"Similar to Chromium's Setting -> On start-up -> Continue where you left off.
|
||
|
||
If non-nil, all active EAF Browser buffers will be saved before Emacs is killed,
|
||
and will re-open them when calling `eaf-browser-restore-buffers' in the future session."
|
||
:type 'boolean)
|
||
|
||
(defcustom eaf-proxy-host ""
|
||
"Proxy Host used by EAF Browser."
|
||
:type 'string)
|
||
|
||
(defcustom eaf-proxy-port ""
|
||
"Proxy Port used by EAF Browser."
|
||
:type 'string)
|
||
|
||
(defcustom eaf-proxy-type ""
|
||
"Proxy Type used by EAF Browser. The value is either \"http\" or \"socks5\"."
|
||
:type 'string)
|
||
|
||
(defcustom eaf-elfeed-split-direction "below"
|
||
"Elfeed browser page display location.
|
||
Default is `below', you can chang it with `right'."
|
||
:type 'string)
|
||
|
||
(defcustom eaf-enable-debug nil
|
||
"If you got segfault error, please turn this option.
|
||
Then EAF will start by gdb, please send new issue with `*eaf*' buffer content when next crash."
|
||
:type 'boolean)
|
||
|
||
(defcustom eaf-wm-name ""
|
||
"The desktop name, set by function `eaf--get-current-desktop-name'."
|
||
:type 'string)
|
||
|
||
(defcustom eaf-wm-focus-fix-wms
|
||
`(
|
||
"i3" ;i3
|
||
"LG3D" ;qtile
|
||
"Xpra"
|
||
)
|
||
"Set mouse cursor to frame bottom in these wms, to make EAF receive input event.
|
||
Add NAME of command `wmctrl -m' to this list."
|
||
:type 'list)
|
||
|
||
(defvar eaf-app-binding-alist
|
||
'(("browser" . eaf-browser-keybinding)
|
||
("pdf-viewer" . eaf-pdf-viewer-keybinding)
|
||
("video-player" . eaf-video-player-keybinding)
|
||
("js-video-player" . eaf-js-video-player-keybinding)
|
||
("image-viewer" . eaf-image-viewer-keybinding)
|
||
("camera" . eaf-camera-keybinding)
|
||
("terminal" . eaf-terminal-keybinding)
|
||
("markdown-previewer" . eaf-browser-keybinding)
|
||
("org-previewer" . eaf-browser-keybinding)
|
||
("mindmap" . eaf-mindmap-keybinding)
|
||
("jupyter" . eaf-jupyter-keybinding)
|
||
)
|
||
"Mapping app names to keybinding variables.
|
||
|
||
Any new app should add the its name and the corresponding
|
||
keybinding variable to this list.")
|
||
|
||
(defvar eaf-app-display-function-alist
|
||
'(("markdown-previewer" . eaf--markdown-preview-display)
|
||
("org-previewer" . eaf--org-preview-display))
|
||
"Mapping app names to display functions.
|
||
|
||
Display functions are called to initilize the initial view when
|
||
starting an app.
|
||
|
||
A display function receives the initialized app buffer as
|
||
argument and defaults to `switch-to-buffer'.")
|
||
|
||
|
||
(defvar eaf-app-bookmark-handlers-alist
|
||
'(("browser" . eaf--browser-bookmark)
|
||
("pdf-viewer" . eaf--pdf-viewer-bookmark))
|
||
"Mapping app names to bookmark handler functions.
|
||
|
||
A bookmark handler function is used as
|
||
`bookmark-make-record-function' and should follow its spec.")
|
||
|
||
(defvar eaf-app-extensions-alist
|
||
'(("pdf-viewer" . eaf-pdf-extension-list)
|
||
("markdown-previewer" . eaf-markdown-extension-list)
|
||
("image-viewer" . eaf-image-extension-list)
|
||
("video-player" . eaf-video-extension-list)
|
||
("browser" . eaf-browser-extension-list)
|
||
("org-previewer" . eaf-org-extension-list)
|
||
("mindmap" . eaf-mindmap-extension-list)
|
||
("office" . eaf-office-extension-list))
|
||
"Mapping app names to extension list variables.
|
||
|
||
A new app can use this to configure extensions which should
|
||
handled by it.")
|
||
|
||
(defvar eaf--monitor-configuration-p t
|
||
"When this variable is non-nil, `eaf-monitor-configuration-change' executes.
|
||
This variable is used to open buffer in backend and avoid graphics blink.
|
||
|
||
EAF call python method `new_buffer' to create EAF application buffer.
|
||
EAF call python method `update_views' to create EAF application view.
|
||
|
||
Python process only create application view when Emacs window or buffer state change.")
|
||
|
||
(defvar eaf-fullscreen-p nil
|
||
"When non-nil, EAF will intelligently hide modeline as necessray.")
|
||
|
||
(defvar eaf-buffer-title-format "%s")
|
||
|
||
(defvar eaf-pdf-outline-buffer-name "*eaf pdf outline*"
|
||
"The name of pdf-outline-buffer.")
|
||
|
||
(defvar eaf-pdf-outline-window-configuration nil
|
||
"Save window configure before popup outline buffer.")
|
||
|
||
(defvar-local eaf--bookmark-title nil)
|
||
|
||
(defvar-local eaf-mindmap--current-add-mode nil)
|
||
|
||
(defmacro eaf-for-each-eaf-buffer (&rest body)
|
||
"A syntactic sugar to loop through each EAF buffer and evaluat BODY.
|
||
|
||
Within BODY, `buffer' can be used to"
|
||
`(dolist (buffer (eaf--get-eaf-buffers))
|
||
(with-current-buffer buffer
|
||
,@body)))
|
||
|
||
(defun eaf-browser-restore-buffers ()
|
||
"EAF restore all opened EAF Browser buffers in the previous Emacs session.
|
||
|
||
This should be used after setting `eaf-browser-continue-where-left-off' to t."
|
||
(interactive)
|
||
(if eaf-browser-continue-where-left-off
|
||
(let* ((browser-restore-file-path
|
||
(concat eaf-config-location
|
||
(file-name-as-directory "browser")
|
||
(file-name-as-directory "history")
|
||
"restore.txt"))
|
||
(browser-url-list
|
||
(with-temp-buffer (insert-file-contents browser-restore-file-path)
|
||
(split-string (buffer-string) "\n" t))))
|
||
(if (epc:live-p eaf-epc-process)
|
||
(dolist (url browser-url-list)
|
||
(eaf-open-browser url))
|
||
(dolist (url browser-url-list)
|
||
(push `(,url "browser" "") eaf--active-buffers))
|
||
(when eaf--active-buffers (eaf-open-browser (nth 0 (car eaf--active-buffers))))))
|
||
(user-error "Please set `eaf-browser-continue-where-left-off' to t first!")))
|
||
|
||
|
||
(defun eaf--bookmark-make-record ()
|
||
"Create a EAF bookmark.
|
||
|
||
The bookmark will try to recreate EAF buffer session.
|
||
For now only EAF browser app is supported."
|
||
(let ((handler (cdr
|
||
(assoc eaf--buffer-app-name
|
||
eaf-app-bookmark-handlers-alist))))
|
||
(when handler
|
||
(funcall handler))))
|
||
|
||
(defun eaf--browser-bookmark ()
|
||
"Restore EAF buffer according to browser bookmark from the current file path or web URL."
|
||
`((handler . eaf--bookmark-restore)
|
||
(eaf-app . "browser")
|
||
(defaults . ,(list eaf--bookmark-title))
|
||
(filename . ,(eaf-get-path-or-url))))
|
||
|
||
(defun eaf--browser-chrome-bookmark (name url)
|
||
"Restore EAF buffer according to chrome bookmark of given title and web URL."
|
||
`((handler . eaf--bookmark-restore)
|
||
(eaf-app . "browser")
|
||
(defaults . ,(list name))
|
||
(filename . ,url)))
|
||
|
||
(defun eaf--pdf-viewer-bookmark ()
|
||
"Restore EAF buffer according to pdf bookmark from the current file path or web URL."
|
||
`((handler . eaf--bookmark-restore)
|
||
(eaf-app . "pdf-viewer")
|
||
(defaults . ,(list eaf--bookmark-title))
|
||
(filename . ,(eaf-get-path-or-url))))
|
||
|
||
(defun eaf--bookmark-restore (bookmark)
|
||
"Restore EAF buffer according to BOOKMARK."
|
||
(let ((app (cdr (assq 'eaf-app bookmark))))
|
||
(cond ((equal app "browser")
|
||
(eaf-open-browser (cdr (assq 'filename bookmark))))
|
||
((equal app "pdf-viewer")
|
||
(eaf-open (cdr (assq 'filename bookmark)))))))
|
||
|
||
;;;###autoload
|
||
(defun eaf-open-bookmark ()
|
||
"Command to open or create EAF bookmarks with completion."
|
||
(interactive)
|
||
(bookmark-maybe-load-default-file)
|
||
(let* ((bookmarks (cl-remove-if-not
|
||
(lambda (entry)
|
||
(bookmark-prop-get entry 'eaf-app))
|
||
bookmark-alist))
|
||
(names (mapcar #'car bookmarks))
|
||
(cand (completing-read "EAF Bookmarks: " bookmarks)))
|
||
(cond ((member cand names)
|
||
(bookmark-jump cand))
|
||
(t
|
||
(unless (derived-mode-p 'eaf-mode)
|
||
(message "This command can only be called in an EAF buffer!"))
|
||
;; create new one for current buffer with provided name
|
||
(bookmark-set cand)))))
|
||
|
||
(defun eaf-import-chrome-bookmarks ()
|
||
"Command to import chrome bookmarks."
|
||
(interactive)
|
||
(when (eaf-read-input "Are you sure to import chrome bookmarks to EAF" "yes-or-no" "")
|
||
(if (not (file-exists-p eaf-chrome-bookmark-file))
|
||
(message "Chrome bookmark file: '%s' is not exist, check `eaf-chrome-bookmark-file` setting." eaf-chrome-bookmark-file)
|
||
(let ((orig-bookmark-record-fn bookmark-make-record-function)
|
||
(data (json-read-file eaf-chrome-bookmark-file)))
|
||
(cl-labels ((fn (item)
|
||
(pcase (alist-get 'type item)
|
||
("url"
|
||
(let ((name (alist-get 'name item))
|
||
(url (alist-get 'url item)))
|
||
(if (not (equal "chrome://bookmarks/" url))
|
||
(progn
|
||
(setq-local bookmark-make-record-function
|
||
#'(lambda () (eaf--browser-chrome-bookmark name url)))
|
||
(bookmark-set name)))))
|
||
("folder"
|
||
(mapc #'fn (alist-get 'children item))))))
|
||
(fn (alist-get 'bookmark_bar (alist-get 'roots data)))
|
||
(setq-local bookmark-make-record-function orig-bookmark-record-fn)
|
||
(bookmark-save)
|
||
(message "Import success."))))))
|
||
|
||
(defalias 'eaf--browser-firefox-bookmark 'eaf--browser-chrome-bookmark)
|
||
|
||
(defvar eaf--existing-bookmarks nil
|
||
"Existing bookmarks in Emacs.
|
||
A hashtable, key is url and value is title.")
|
||
|
||
(defvar eaf--firefox-bookmarks nil
|
||
"Bookmarks that should be imported from firefox.")
|
||
|
||
(defun eaf--load-existing-bookmarks()
|
||
"Load existing bookmarks."
|
||
(let ((bookmarks (make-hash-table :test 'equal)))
|
||
(dolist (bm bookmark-alist)
|
||
(let* ((name (car bm))
|
||
(file (bookmark-get-filename name)))
|
||
(puthash file name bookmarks)))
|
||
bookmarks))
|
||
|
||
(defun eaf--useful-firefox-bookmark? (uri)
|
||
"Check whether uri is a website url."
|
||
(or (string-prefix-p "http://" uri)
|
||
(string-prefix-p "https://" uri)))
|
||
|
||
(defun eaf--firefox-bookmark-to-import? (title uri)
|
||
"Check whether uri should be imported."
|
||
(when (eaf--useful-firefox-bookmark? uri)
|
||
(let ((old (gethash uri eaf--existing-bookmarks)))
|
||
(when (or
|
||
(not old)
|
||
(and (string-equal old "") (not (string-equal title ""))))
|
||
t))))
|
||
|
||
(defun eaf--firefox-bookmark-to-import (title uri)
|
||
(puthash uri title eaf--existing-bookmarks)
|
||
(add-to-list 'eaf--firefox-bookmarks (cons uri title)))
|
||
|
||
(defun eaf-import-firefox-bookmarks ()
|
||
"Command to import firefox bookmarks."
|
||
(interactive)
|
||
(when (eaf-read-input "In order to import, you should first backup firefox's bookmarks to a json file. Continue?" "yes-or-no" "")
|
||
(let ((fx-bookmark-file (read-file-name "Choose firefox bookmark file:")))
|
||
(if (not (file-exists-p fx-bookmark-file))
|
||
(message "Firefox bookmark file: '%s' is not exist." fx-bookmark-file)
|
||
(setq eaf--firefox-bookmarks nil)
|
||
(setq eaf--existing-bookmarks (eaf--load-existing-bookmarks))
|
||
(let ((orig-bookmark-record-fn bookmark-make-record-function)
|
||
(data (json-read-file fx-bookmark-file)))
|
||
(cl-labels ((fn (item)
|
||
(pcase (alist-get 'typeCode item)
|
||
(1
|
||
(let ((title (alist-get 'title item ""))
|
||
(uri (alist-get 'uri item)))
|
||
(when (eaf--firefox-bookmark-to-import? title uri)
|
||
(eaf--firefox-bookmark-to-import title uri))))
|
||
(2
|
||
(mapc #'fn (alist-get 'children item))))))
|
||
(fn data)
|
||
(dolist (bm eaf--firefox-bookmarks)
|
||
(let ((uri (car bm))
|
||
(title (cdr bm)))
|
||
(setq-local bookmark-make-record-function
|
||
#'(lambda () (eaf--browser-firefox-bookmark title uri)))
|
||
(bookmark-set title)))
|
||
(setq-local bookmark-make-record-function orig-bookmark-record-fn)
|
||
(bookmark-save)
|
||
(message "Import success.")))))))
|
||
|
||
(defun eaf-open-external ()
|
||
"Command to open current path or url with external application."
|
||
(interactive)
|
||
(let ((path-or-url (eaf-get-path-or-url)))
|
||
(cond ((memq system-type '(cygwin windows-nt ms-dos))
|
||
(w32-shell-execute "open" path-or-url))
|
||
((eq system-type 'darwin)
|
||
(concat "open " (shell-quote-argument path-or-url)))
|
||
((eq system-type 'gnu/linux)
|
||
(let ((process-connection-type nil))
|
||
(start-process "" nil "xdg-open" path-or-url))))))
|
||
|
||
(defun eaf-call-async (method &rest args)
|
||
"Call Python EPC function METHOD and ARGS asynchronously."
|
||
(deferred:$
|
||
(epc:call-deferred eaf-epc-process (read method) args)))
|
||
|
||
(defun eaf-call-sync (method &rest args)
|
||
"Call Python EPC function METHOD and ARGS synchronously."
|
||
(epc:call-sync eaf-epc-process (read method) args))
|
||
|
||
(defun eaf-get-emacs-xid (frame)
|
||
"Get Emacs FRAME xid."
|
||
(frame-parameter frame 'window-id))
|
||
|
||
(defun eaf-serialization-var-list ()
|
||
"Serialize variable list."
|
||
(json-encode eaf-var-list))
|
||
|
||
(defun eaf-start-process ()
|
||
"Start EAF process if it isn't started."
|
||
(cond
|
||
((not eaf--active-buffers)
|
||
(user-error "[EAF] Please initiate EAF with eaf-open-... functions only"))
|
||
((epc:live-p eaf-epc-process)
|
||
(user-error "[EAF] Process is already running")))
|
||
(let* ((eaf-args (append
|
||
(list eaf-python-file)
|
||
(eaf-get-render-size)
|
||
(list eaf-proxy-host eaf-proxy-port eaf-proxy-type)
|
||
(list eaf-config-location)
|
||
(list (number-to-string eaf-server-port))
|
||
(list (eaf-serialization-var-list))
|
||
))
|
||
(gdb-args (list "-batch" "-ex" "run" "-ex" "bt" "--args" eaf-python-command))
|
||
(process-environment (cl-copy-list process-environment)))
|
||
|
||
(let ((wayland-display (getenv "WAYLAND_DISPLAY")))
|
||
(when (and wayland-display (not (string= wayland-display "")))
|
||
(setenv "QT_QPA_PLATFORM" "xcb")))
|
||
|
||
;; Start emacs server.
|
||
(eaf-server-start eaf-server-port)
|
||
|
||
;; Start python process.
|
||
(if eaf-enable-debug
|
||
(progn
|
||
(setq eaf-internal-process-prog "gdb")
|
||
(setq eaf-internal-process-args (append gdb-args eaf-args)))
|
||
(setq eaf-internal-process-prog eaf-python-command)
|
||
(setq eaf-internal-process-args eaf-args))
|
||
|
||
(setq eaf-internal-process
|
||
(apply 'start-process
|
||
eaf-name eaf-name
|
||
eaf-internal-process-prog eaf-internal-process-args))
|
||
(set-process-query-on-exit-flag eaf-internal-process nil))
|
||
(message "[EAF] Process starting..."))
|
||
|
||
(defun eaf-stop-process (&optional restart)
|
||
"Stop EAF process and kill all EAF buffers.
|
||
|
||
If RESTART is non-nil, cached URL and app-name will not be cleared."
|
||
(interactive)
|
||
(unless restart
|
||
;; Clear active buffers
|
||
(setq eaf--active-buffers nil)
|
||
;; Remove all EAF related hooks since the EAF process is stopped.
|
||
(remove-hook 'kill-buffer-hook #'eaf--monitor-buffer-kill)
|
||
(remove-hook 'kill-emacs-hook #'eaf--monitor-emacs-kill)
|
||
(remove-hook 'after-save-hook #'eaf--org-preview-monitor-buffer-save)
|
||
(remove-hook 'kill-buffer-hook #'eaf--org-preview-monitor-kill)
|
||
(remove-hook 'window-size-change-functions #'eaf-monitor-window-size-change)
|
||
(remove-hook 'window-configuration-change-hook #'eaf-monitor-configuration-change)
|
||
(eaf-server-stop eaf-server-port))
|
||
|
||
;; Clean `eaf-org-file-list' and `eaf-org-killed-file-list'.
|
||
(dolist (org-file-name eaf-org-file-list)
|
||
(eaf--delete-org-preview-file org-file-name))
|
||
(setq eaf-org-file-list nil)
|
||
(setq eaf-org-killed-file-list nil)
|
||
(setq-local eaf-fullscreen-p nil)
|
||
|
||
;; Kill EAF-mode buffers.
|
||
(let* ((eaf-buffers (eaf--get-eaf-buffers))
|
||
(count (length eaf-buffers)))
|
||
(dolist (buffer eaf-buffers)
|
||
(kill-buffer buffer))
|
||
;; Just report to me when EAF buffer exists.
|
||
(message "[EAF] Killed %s EAF buffer%s" count (if (> count 1) "s!" "!")))
|
||
|
||
;; Kill process after kill buffer, make application can save session data.
|
||
(eaf--kill-python-process))
|
||
|
||
(defalias 'eaf-kill-process #'eaf-stop-process)
|
||
|
||
(defun eaf--kill-python-process ()
|
||
"Kill EAF background python process."
|
||
(interactive)
|
||
(if (epc:live-p eaf-epc-process)
|
||
;; Delete EAF server process.
|
||
(progn
|
||
(epc:stop-epc eaf-epc-process)
|
||
;; Kill *eaf* buffer.
|
||
(when (get-buffer eaf-name)
|
||
(kill-buffer eaf-name))
|
||
(message "[EAF] Process terminated."))
|
||
(message "[EAF] Process already terminated.")))
|
||
|
||
(defun eaf-restart-process ()
|
||
"Stop and restart EAF process."
|
||
(interactive)
|
||
(setq eaf--active-buffers nil)
|
||
(eaf-for-each-eaf-buffer
|
||
(push `(,eaf--buffer-url ,eaf--buffer-app-name ,eaf--buffer-args) eaf--active-buffers))
|
||
(eaf-stop-process t)
|
||
(eaf-start-process))
|
||
|
||
(defun eaf-get-render-size ()
|
||
"Get allocation for render application in backend.
|
||
We need calcuate render allocation to make sure no black border around render content."
|
||
(let* (;; We use `window-inside-pixel-edges' and `window-absolute-pixel-edges' calcuate height of window header, such as tabbar.
|
||
(window-header-height (- (nth 1 (window-inside-pixel-edges)) (nth 1 (window-absolute-pixel-edges))))
|
||
(width (frame-pixel-width))
|
||
;; Render height should minus mode-line height, minibuffer height, header height.
|
||
(height (- (frame-pixel-height) (window-mode-line-height) (window-pixel-height (minibuffer-window)) window-header-height)))
|
||
(mapcar (lambda (x) (format "%s" x)) (list width height))))
|
||
|
||
(defun eaf-get-window-allocation (&optional window)
|
||
"Get WINDOW allocation."
|
||
(let* ((window-edges (window-pixel-edges window))
|
||
(x (nth 0 window-edges))
|
||
(y (+ (nth 1 window-edges)
|
||
(if (version< emacs-version "27.0")
|
||
(window-header-line-height window)
|
||
(window-tab-line-height window))
|
||
(if (and (require 'tab-line nil t)
|
||
tab-line-mode) ; Support emacs 27 tab-line-mode
|
||
(window-tab-line-height window)
|
||
0)))
|
||
(w (- (nth 2 window-edges) x))
|
||
(h (- (nth 3 window-edges) (window-mode-line-height window) y)))
|
||
(list x y w h)))
|
||
|
||
(defun eaf--generate-id ()
|
||
"Randomly generate a seven digit id used for EAF buffers."
|
||
(format "%04x-%04x-%04x-%04x-%04x-%04x-%04x"
|
||
(random (expt 16 4))
|
||
(random (expt 16 4))
|
||
(random (expt 16 4))
|
||
(random (expt 16 4))
|
||
(random (expt 16 4))
|
||
(random (expt 16 4))
|
||
(random (expt 16 4))))
|
||
|
||
(defun eaf-execute-app-cmd (cmd &optional buf)
|
||
"Execute app CMD.
|
||
|
||
If BUF is given it should be the EAF buffer for the command
|
||
otherwise it is assumed that the current buffer is the EAF
|
||
buffer."
|
||
(with-current-buffer (or buf (current-buffer))
|
||
(let ((this-command cmd))
|
||
(call-interactively cmd))))
|
||
|
||
(defun eaf-get-path-or-url ()
|
||
"Get the current file path or web URL.
|
||
|
||
When called interactively, copy to ‘kill-ring’."
|
||
(interactive)
|
||
(if (derived-mode-p 'eaf-mode)
|
||
(if (called-interactively-p 'any)
|
||
(message "%s" (kill-new (eaf-call-sync "call_function" eaf--buffer-id "get_url")))
|
||
(eaf-call-sync "call_function" eaf--buffer-id "get_url"))
|
||
(user-error "This command can only be called in an EAF buffer!")))
|
||
|
||
(defun eaf-toggle-fullscreen ()
|
||
"Toggle fullscreen."
|
||
(interactive)
|
||
(eaf-call-async "execute_function" eaf--buffer-id "toggle_fullscreen" (key-description (this-command-keys-vector))))
|
||
|
||
(defun eaf-share-path-or-url ()
|
||
"Share the current file path or web URL as QRCode."
|
||
(interactive)
|
||
(eaf-open (eaf-get-path-or-url) "airshare"))
|
||
|
||
(defun eaf--make-proxy-function (fun)
|
||
"Define elisp command which can call python function string FUN."
|
||
(let ((sym (intern (format "eaf-proxy-%s" fun))))
|
||
(unless (fboundp sym)
|
||
(defalias sym
|
||
(lambda nil
|
||
(interactive)
|
||
;; Ensure this is only called from EAF buffer
|
||
(if (derived-mode-p 'eaf-mode)
|
||
(eaf-call-async "execute_function" eaf--buffer-id fun (key-description (this-command-keys-vector)))
|
||
(message "%s command can only be called in an EAF buffer!" sym)))
|
||
(format
|
||
"Proxy function to call \"%s\" on the Python side.
|
||
|
||
Use `eaf-execute-app-cmd' if you want to execute this command programmatically.
|
||
Please ONLY use `eaf-bind-key' and use the unprefixed command name (\"%s\")
|
||
to edit EAF keybindings!" fun fun)))
|
||
sym))
|
||
|
||
(defun eaf--gen-keybinding-map (keybinding &optional no-inherit-eaf-mode-map*)
|
||
"Configure the `eaf-mode-map' from KEYBINDING, one of the eaf-.*-keybinding variables."
|
||
(setq eaf-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(unless no-inherit-eaf-mode-map*
|
||
(set-keymap-parent map eaf-mode-map*))
|
||
(cl-loop for (key . fun) in keybinding
|
||
do (define-key map (kbd key)
|
||
(cond
|
||
;; If command is normal symbol, just call it directly.
|
||
((symbolp fun)
|
||
fun)
|
||
;; If command is string and include - , it's elisp function, use `intern' build elisp function from function name.
|
||
((string-match "-" fun)
|
||
(intern fun))
|
||
;; If command is not built-in function and not include char '-'
|
||
;; it's command in python side, build elisp proxy function to call it.
|
||
(t
|
||
(eaf--make-proxy-function fun))
|
||
))
|
||
finally return map)))
|
||
)
|
||
|
||
(defun eaf--toggle-caret-browsing (caret-status)
|
||
"Toggle caret browsing given CARET-STATUS."
|
||
(if caret-status
|
||
(eaf--gen-keybinding-map eaf-browser-caret-mode-keybinding t)
|
||
(eaf--gen-keybinding-map eaf-browser-keybinding))
|
||
(setq eaf--buffer-map-alist (list (cons t eaf-mode-map))))
|
||
|
||
(defun eaf--get-app-bindings (app-name)
|
||
"Get the specified APP-NAME keybinding.
|
||
|
||
Every app has its name and the corresponding
|
||
keybinding variable to eaf-app-binding-alist."
|
||
(symbol-value
|
||
(cdr (assoc app-name eaf-app-binding-alist))))
|
||
|
||
(defun eaf--create-buffer (url app-name args)
|
||
"Create an EAF buffer given URL, APP-NAME, and ARGS."
|
||
(eaf--gen-keybinding-map (eaf--get-app-bindings app-name))
|
||
(let* ((eaf-buffer-name (if (equal (file-name-nondirectory url) "")
|
||
url
|
||
(file-name-nondirectory url)))
|
||
(eaf-buffer (generate-new-buffer eaf-buffer-name))
|
||
(url-directory (or (file-name-directory url) url)))
|
||
(with-current-buffer eaf-buffer
|
||
(eaf-mode)
|
||
(when (file-accessible-directory-p url-directory)
|
||
(setq-local default-directory url-directory))
|
||
;; `eaf-buffer-url' should record full path of url, otherwise `eaf-open' will open duplicate PDF tab for same url.
|
||
(set (make-local-variable 'eaf--buffer-url) url)
|
||
(set (make-local-variable 'eaf--buffer-app-name) app-name)
|
||
(set (make-local-variable 'eaf--buffer-args) args)
|
||
(run-hooks (intern (format "eaf-%s-hook" app-name)))
|
||
(setq mode-name (concat "EAF/" app-name)))
|
||
eaf-buffer))
|
||
|
||
(defun eaf-monitor-window-size-change (frame)
|
||
"Delay some time and run `eaf-try-adjust-view-with-frame-size' to compare with Emacs FRAME size."
|
||
(when (epc:live-p eaf-epc-process)
|
||
(setq eaf-last-frame-width (frame-pixel-width frame))
|
||
(setq eaf-last-frame-height (frame-pixel-height frame))
|
||
(run-with-timer 1 nil (lambda () (eaf-try-adjust-view-with-frame-size frame)))))
|
||
|
||
(defun eaf-try-adjust-view-with-frame-size (frame)
|
||
"Update EAF view once Emacs window size of the FRAME is changed."
|
||
(unless (and (equal (frame-pixel-width frame) eaf-last-frame-width)
|
||
(equal (frame-pixel-height frame) eaf-last-frame-height))
|
||
(eaf-monitor-configuration-change)))
|
||
|
||
(defun eaf-monitor-configuration-change (&rest _)
|
||
"EAF function to respond when detecting a window configuration change."
|
||
(when (and eaf--monitor-configuration-p
|
||
(epc:live-p eaf-epc-process))
|
||
(ignore-errors
|
||
(let (view-infos)
|
||
(dolist (frame (frame-list))
|
||
(dolist (window (window-list frame))
|
||
(with-current-buffer (window-buffer window)
|
||
(when (derived-mode-p 'eaf-mode)
|
||
;; When `eaf-fullscreen-p' is non-nil, and only the EAF window is present, use frame size
|
||
(if (and eaf-fullscreen-p (equal (length (window-list frame)) 1))
|
||
(push (format "%s:%s:%s:%s:%s:%s"
|
||
eaf--buffer-id
|
||
(eaf-get-emacs-xid frame)
|
||
0 0 (frame-pixel-width frame) (frame-pixel-height frame))
|
||
view-infos)
|
||
(let* ((window-allocation (eaf-get-window-allocation window))
|
||
(x (nth 0 window-allocation))
|
||
(y (nth 1 window-allocation))
|
||
(w (nth 2 window-allocation))
|
||
(h (nth 3 window-allocation)))
|
||
(push (format "%s:%s:%s:%s:%s:%s"
|
||
eaf--buffer-id
|
||
(eaf-get-emacs-xid frame)
|
||
x y w h)
|
||
view-infos)))))))
|
||
(eaf-call-async "update_views" (mapconcat #'identity view-infos ","))))))
|
||
|
||
(defun eaf--delete-org-preview-file (org-file)
|
||
"Delete the org-preview file when given ORG-FILE name."
|
||
(let ((org-html-file (concat (file-name-sans-extension org-file) ".html")))
|
||
(when (file-exists-p org-html-file)
|
||
(delete-file org-html-file)
|
||
(message "[EAF] Cleaned org-preview file %s (%s)." org-html-file org-file))))
|
||
|
||
(defun eaf--org-killed-buffer-clean ()
|
||
"Function cleaning the killed org buffer."
|
||
(dolist (org-killed-buffer eaf-org-killed-file-list)
|
||
(unless (get-file-buffer org-killed-buffer)
|
||
(setq eaf-org-file-list (remove org-killed-buffer eaf-org-file-list))
|
||
(eaf--delete-org-preview-file org-killed-buffer)))
|
||
(setq eaf-org-killed-file-list nil))
|
||
|
||
(defun eaf--get-eaf-buffers ()
|
||
"A function that return a list of EAF buffers."
|
||
(cl-remove-if-not
|
||
(lambda (buffer)
|
||
(with-current-buffer buffer
|
||
(derived-mode-p 'eaf-mode)))
|
||
(buffer-list)))
|
||
|
||
(defun eaf--monitor-buffer-kill ()
|
||
"A function monitoring when an EAF buffer is killed."
|
||
(ignore-errors
|
||
(eaf-call-async "kill_buffer" eaf--buffer-id))
|
||
|
||
;; Kill eaf process when last eaf buffer closed.
|
||
;; We need add timer to avoid the last web page kill when terminal is exited.
|
||
(run-at-time
|
||
5 nil
|
||
(lambda ()
|
||
(when (equal (length (eaf--get-eaf-buffers)) 0)
|
||
(eaf--kill-python-process))
|
||
)))
|
||
|
||
(defun eaf--monitor-emacs-kill ()
|
||
"Function monitoring when Emacs is killed."
|
||
(ignore-errors
|
||
(when eaf-browser-continue-where-left-off
|
||
(let* ((browser-restore-file-path
|
||
(concat eaf-config-location
|
||
(file-name-as-directory "browser")
|
||
(file-name-as-directory "history")
|
||
"restore.txt"))
|
||
(browser-urls ""))
|
||
(write-region
|
||
(dolist (buffer (eaf--get-eaf-buffers) browser-urls)
|
||
(with-current-buffer buffer
|
||
(when (equal eaf--buffer-app-name "browser")
|
||
(setq browser-urls (concat eaf--buffer-url "\n" browser-urls)))))
|
||
nil browser-restore-file-path)))
|
||
(eaf-call-async "kill_emacs")))
|
||
|
||
(defun eaf--org-preview-monitor-kill ()
|
||
"Function monitoring when org-preview application is killed."
|
||
;; Because save org buffer will trigger `kill-buffer' action,
|
||
;; but org buffer still live after do `kill-buffer' action.
|
||
;; So I run a timer to check org buffer is live after `kill-buffer' action.
|
||
(when (member (buffer-file-name) eaf-org-file-list)
|
||
(unless (member (buffer-file-name) eaf-org-killed-file-list)
|
||
(push (buffer-file-name) eaf-org-killed-file-list))
|
||
(run-with-timer 1 nil (lambda () (eaf--org-killed-buffer-clean)))))
|
||
|
||
|
||
(defun eaf--org-preview-monitor-buffer-save ()
|
||
"Save org-preview buffer."
|
||
(when (epc:live-p eaf-epc-process)
|
||
(ignore-errors
|
||
;; eaf-org-file-list?
|
||
(org-html-export-to-html)
|
||
(eaf-call-async "update_buffer_with_url" "app.org-previewer.buffer" (buffer-file-name) "")
|
||
(message "[EAF] Export %s to HTML." (buffer-file-name)))))
|
||
|
||
(defun eaf-keyboard-quit ()
|
||
"Wrap around `keyboard-quit' and signals a ‘quit’ condition to EAF applications."
|
||
(interactive)
|
||
(eaf-call-async "action_quit" eaf--buffer-id)
|
||
(call-interactively 'keyboard-quit))
|
||
|
||
(defun eaf-send-key ()
|
||
"Directly send key to EAF Python side."
|
||
(interactive)
|
||
(eaf-call-async "send_key" eaf--buffer-id (key-description (this-command-keys-vector))))
|
||
|
||
(defun eaf-send-left-key ()
|
||
"Directly send left key to EAF Python side."
|
||
(interactive)
|
||
(eaf-call-async "send_key" eaf--buffer-id "<left>"))
|
||
|
||
(defun eaf-send-right-key ()
|
||
"Directly send right key to EAF Python side."
|
||
(interactive)
|
||
(eaf-call-async "send_key" eaf--buffer-id "<right>"))
|
||
|
||
(defun eaf-send-down-key ()
|
||
"Directly send down key to EAF Python side."
|
||
(interactive)
|
||
(eaf-call-async "send_key" eaf--buffer-id "<down>"))
|
||
|
||
(defun eaf-send-up-key ()
|
||
"Directly send up key to EAF Python side."
|
||
(interactive)
|
||
(eaf-call-async "send_key" eaf--buffer-id "<up>"))
|
||
|
||
(defun eaf-send-return-key ()
|
||
"Directly send return key to EAF Python side."
|
||
(interactive)
|
||
(eaf-call-async "send_key" eaf--buffer-id "RET"))
|
||
|
||
(defun eaf-send-key-sequence ()
|
||
"Directly send key sequence to EAF Python side."
|
||
(interactive)
|
||
(eaf-call-async "send_key_sequence" eaf--buffer-id (key-description (this-command-keys-vector))))
|
||
|
||
(defun eaf-send-ctrl-return-sequence ()
|
||
"Directly send Ctrl-Return key sequence to EAF Python side."
|
||
(interactive)
|
||
(eaf-call-async "send_key_sequence" eaf--buffer-id "C-RET"))
|
||
|
||
(defun eaf-send-alt-backspace-sequence ()
|
||
"Directly send Alt-Backspace key sequence to EAF Python side."
|
||
(interactive)
|
||
(eaf-call-async "send_key_sequence" eaf--buffer-id "M-<backspace>"))
|
||
|
||
(defun eaf-send-shift-return-sequence ()
|
||
"Directly send Shift-Return key sequence to EAF Python side."
|
||
(interactive)
|
||
(eaf-call-async "send_key_sequence" eaf--buffer-id "S-RET"))
|
||
|
||
(defun eaf-send-second-key-sequence ()
|
||
"Send second part of key sequence to terminal."
|
||
(interactive)
|
||
(eaf-call-async "send_key_sequence"
|
||
eaf--buffer-id
|
||
(nth 1 (split-string (key-description (this-command-keys-vector))))))
|
||
|
||
(defun eaf-set (sym val)
|
||
"Similar to `set', but store SYM with VAL in EAF Python side, and return VAL.
|
||
|
||
For convenience, use the Lisp macro `eaf-setq' instead."
|
||
(setf (map-elt eaf-var-list sym) val)
|
||
(when (epc:live-p eaf-epc-process)
|
||
;; Update python side variable dynamically.
|
||
(eaf-call-async "update_emacs_var_dict" (eaf-serialization-var-list)))
|
||
val)
|
||
|
||
(defmacro eaf-setq (var val)
|
||
"Similar to `setq', but store VAR with VAL in EAF Python side, and return VAL.
|
||
|
||
Use it as (eaf-setq var val)"
|
||
`(eaf-set ',var ,val))
|
||
|
||
(defmacro eaf-bind-key (command key eaf-app-keybinding)
|
||
"This function binds COMMAND to KEY in EAF-APP-KEYBINDING list.
|
||
|
||
Use this to bind keys for EAF applications.
|
||
|
||
COMMAND is a symbol of a regular Emacs command or a python app
|
||
command. You can see a list of available commands by calling
|
||
`eaf-describe-bindings' in an EAF buffer. The `eaf-proxy-' prefix
|
||
should be dropped for the COMMAND symbol.
|
||
|
||
KEY is a string representing a sequence of keystrokes and events.
|
||
|
||
EAF-APP-KEYBINDING is one of the `eaf-<app-name>-keybinding'
|
||
variables, where <app-name> can be obtained by checking the value
|
||
of `eaf--buffer-app-name' inside the EAF buffer."
|
||
`(setf (map-elt ,eaf-app-keybinding ,key)
|
||
,(if (string-match "_" (symbol-name command))
|
||
(symbol-name command)
|
||
`(quote ,command))))
|
||
|
||
(defun eaf-focus-buffer (focus-buffer-id)
|
||
"Focus the buffer given the FOCUS-BUFFER-ID."
|
||
(catch 'found-eaf
|
||
(eaf-for-each-eaf-buffer
|
||
(when (string= eaf--buffer-id focus-buffer-id)
|
||
(let ((buffer-window (get-buffer-window buffer)))
|
||
(when buffer-window
|
||
(select-window buffer-window)))
|
||
(throw 'found-eaf t)))))
|
||
|
||
(defun eaf--show-message (format-string)
|
||
(message (concat "[EAF/" eaf--buffer-app-name "] " (base64-decode-string format-string))))
|
||
|
||
(defun eaf--set-emacs-var (name value eaf-specific)
|
||
"Set Lisp variable NAME with VALUE on the Emacs side.
|
||
|
||
If EAF-SPECIFIC is true, this is modifying variables in `eaf-var-list'"
|
||
(if (string= eaf-specific "true")
|
||
(eaf-set (intern name) value)
|
||
(set (intern name) value)))
|
||
|
||
(defun eaf--create-new-browser-buffer (new-window-buffer-id)
|
||
"Function for creating a new browser buffer with the specified NEW-WINDOW-BUFFER-ID."
|
||
(let ((eaf-buffer (generate-new-buffer (concat "Browser Popup Window " new-window-buffer-id))))
|
||
(with-current-buffer eaf-buffer
|
||
(eaf-mode)
|
||
(set (make-local-variable 'eaf--buffer-id) new-window-buffer-id)
|
||
(set (make-local-variable 'eaf--buffer-url) "")
|
||
(set (make-local-variable 'eaf--buffer-app-name) "browser"))
|
||
(switch-to-buffer eaf-buffer)))
|
||
|
||
(defun eaf-request-kill-buffer (kill-buffer-id)
|
||
"Function for requesting to kill the given buffer with KILL-BUFFER-ID."
|
||
(catch 'found-eaf
|
||
(eaf-for-each-eaf-buffer
|
||
(when (string= eaf--buffer-id kill-buffer-id)
|
||
(kill-buffer buffer)
|
||
(throw 'found-eaf t)))))
|
||
|
||
(defun eaf--first-start (eaf-epc-port webengine-include-private-codec)
|
||
"Call `eaf--open-internal' upon receiving `start_finish' signal from server.
|
||
|
||
WEBENGINE-INCLUDE-PRIVATE-CODEC is only useful when app-name is video-player."
|
||
;; Make EPC process.
|
||
(setq eaf-epc-process (make-epc:manager
|
||
:server-process eaf-internal-process
|
||
:commands (cons eaf-internal-process-prog eaf-internal-process-args)
|
||
:title (mapconcat 'identity (cons eaf-internal-process-prog eaf-internal-process-args) " ")
|
||
:port eaf-epc-port
|
||
:connection (epc:connect "localhost" eaf-epc-port)
|
||
))
|
||
(epc:init-epc-layer eaf-epc-process)
|
||
|
||
;; If webengine-include-private-codec and app name is "video-player", replace by "js-video-player".
|
||
(setq eaf--webengine-include-private-codec webengine-include-private-codec)
|
||
(let* ((first-buffer-info (pop eaf--active-buffers))
|
||
(first-start-url (nth 0 first-buffer-info))
|
||
(first-start-app-name (nth 1 first-buffer-info))
|
||
(first-start-args (nth 2 first-buffer-info)))
|
||
(when (and (string-equal first-start-app-name "video-player")
|
||
(string-equal eaf--webengine-include-private-codec "True"))
|
||
(setq first-start-app-name "js-video-player"))
|
||
;; Start first app.
|
||
(eaf--open-internal first-start-url first-start-app-name first-start-args))
|
||
|
||
(dolist (buffer-info eaf--active-buffers)
|
||
(eaf--open-internal (nth 0 buffer-info) (nth 1 buffer-info) (nth 2 buffer-info))))
|
||
|
||
(defun eaf--update-buffer-details (buffer-id title url)
|
||
"Function for updating buffer details with its BUFFER-ID, TITLE and URL."
|
||
(when (> (length title) 0)
|
||
(catch 'found-eaf
|
||
(dolist (window (window-list))
|
||
(let ((buffer (window-buffer window)))
|
||
(with-current-buffer buffer
|
||
(when (and
|
||
(derived-mode-p 'eaf-mode)
|
||
(equal eaf--buffer-id buffer-id))
|
||
(setq mode-name (concat "EAF/" eaf--buffer-app-name))
|
||
(setq-local eaf--bookmark-title title)
|
||
(setq-local eaf--buffer-url url)
|
||
(rename-buffer (format eaf-buffer-title-format title) t)
|
||
(throw 'found-eaf t))))))))
|
||
|
||
(defun eaf-translate-text (text)
|
||
"Use sdcv to translate selected TEXT."
|
||
(when (featurep 'sdcv)
|
||
(sdcv-search-input+ text)))
|
||
|
||
(defun eaf--input-message (input-buffer-id interactive-string callback-tag interactive-type initial-content)
|
||
"Handles input message INTERACTIVE-STRING on the Python side given INPUT-BUFFER-ID and CALLBACK-TYPE."
|
||
(let* ((input-message (eaf-read-input (concat "[EAF/" eaf--buffer-app-name "] " interactive-string) interactive-type initial-content)))
|
||
(if input-message
|
||
(eaf-call-async "handle_input_response" input-buffer-id callback-tag input-message)
|
||
(eaf-call-async "cancel_input_response" input-buffer-id callback-tag))))
|
||
|
||
(defun eaf-read-input (interactive-string interactive-type initial-content)
|
||
"EAF's multi-purpose read-input function which read an INTERACTIVE-STRING with INITIAL-CONTENT, determines the function base on INTERACTIVE-TYPE."
|
||
(condition-case nil
|
||
(cond ((string-equal interactive-type "string")
|
||
(read-string interactive-string initial-content))
|
||
((string-equal interactive-type "file")
|
||
(expand-file-name (read-file-name interactive-string)))
|
||
((string-equal interactive-type "yes-or-no")
|
||
(yes-or-no-p interactive-string)))
|
||
(quit nil)))
|
||
|
||
(defun eaf--open-internal (url app-name args)
|
||
"Open an EAF application internally with URL, APP-NAME and ARGS."
|
||
(let* ((buffer (eaf--create-buffer url app-name args)))
|
||
(with-current-buffer buffer
|
||
(eaf-call-async "new_buffer" eaf--buffer-id url app-name args))
|
||
(eaf--display-app-buffer app-name buffer))
|
||
(eaf--post-open-actions url app-name args))
|
||
|
||
(defun eaf--post-open-actions (url app-name args)
|
||
"The function to run after `eaf--open-internal', taking the same URL, APP-NAME and ARGS."
|
||
(cond ((and args (equal app-name "pdf-viewer"))
|
||
(let ((office-pdf (string-match "office-pdf" args)))
|
||
(when office-pdf
|
||
(with-current-buffer (file-name-nondirectory url)
|
||
(rename-buffer (concat "[Converted] " (substring args 0 (- office-pdf 1))) t)))))))
|
||
|
||
(defun eaf--markdown-preview-display (buf)
|
||
"Given BUF, split window to show file and previewer."
|
||
(eaf-split-preview-windows
|
||
(buffer-local-value
|
||
'eaf--buffer-url buf))
|
||
(switch-to-buffer buf)
|
||
(other-window +1))
|
||
|
||
(defun eaf--org-preview-display (buf)
|
||
"Given BUF, split window to show file and previewer."
|
||
(let ((url (buffer-local-value
|
||
'eaf--buffer-url buf)))
|
||
;; Find file first, because `find-file' will trigger `kill-buffer' operation.
|
||
(save-excursion
|
||
(find-file url)
|
||
(org-html-export-to-html)
|
||
(add-hook 'after-save-hook #'eaf--org-preview-monitor-buffer-save nil t)
|
||
(add-hook 'kill-buffer-hook #'eaf--org-preview-monitor-kill nil t))
|
||
;; Add file name to `eaf-org-file-list' after command `find-file'.
|
||
(unless (member url eaf-org-file-list)
|
||
(push url eaf-org-file-list))
|
||
;; Split window to show file and previewer.
|
||
(eaf-split-preview-windows url)
|
||
;; Switch to new buffer if buffer create successful.
|
||
(switch-to-buffer buf)
|
||
(other-window +1)))
|
||
|
||
(defun eaf--gnus-htmlp (part)
|
||
"Determine whether the gnus mail PART is HTML."
|
||
(when-let ((type (mm-handle-type part)))
|
||
(string= "text/html" (car type))))
|
||
|
||
(defun eaf--notmuch-htmlp (part)
|
||
"Determine whether the notmuch mail PART is HTML."
|
||
(when-let ((type (plist-get part :content-type)))
|
||
(string= "text/html" type)))
|
||
|
||
(defun eaf--get-html-func ()
|
||
"The function returning a function used to extract HTML of different MUAs."
|
||
(catch 'get-html
|
||
(cl-loop for (regex . func) in eaf-mua-get-html
|
||
do (when (string-match regex (symbol-name major-mode))
|
||
(throw 'get-html func))
|
||
finally return (error "[EAF] You are either not in a MUA buffer or your MUA is not supported!"))))
|
||
|
||
(defun eaf-gnus-get-html ()
|
||
"Retrieve HTML part of a gnus mail."
|
||
(with-current-buffer gnus-original-article-buffer
|
||
(when-let* ((dissect (mm-dissect-buffer t t))
|
||
(buffer (if (bufferp (car dissect))
|
||
(when (eaf--gnus-htmlp dissect)
|
||
(car dissect))
|
||
(car (cl-find-if #'eaf--gnus-htmlp (cdr dissect))))))
|
||
(with-current-buffer buffer
|
||
(buffer-string)))))
|
||
|
||
(defun eaf-mu4e-get-html ()
|
||
"Retrieve HTML part of a mu4e mail."
|
||
(let ((msg mu4e~view-message))
|
||
(mu4e-message-field msg :body-html)))
|
||
|
||
(defun eaf-notmuch-get-html ()
|
||
"Retrieve HTML part of a notmuch mail."
|
||
(when-let* ((msg (cond ((derived-mode-p 'notmuch-show-mode)
|
||
(notmuch-show-get-message-properties))
|
||
((derived-mode-p 'notmuch-tree-mode)
|
||
(notmuch-tree-get-message-properties))
|
||
(t nil)))
|
||
(body (plist-get msg :body))
|
||
(parts (car body))
|
||
(content (plist-get parts :content))
|
||
(part (if (listp content)
|
||
(cl-find-if #'eaf--notmuch-htmlp content)
|
||
(when (eaf--notmuch-htmlp parts)
|
||
parts))))
|
||
(notmuch-get-bodypart-text msg part notmuch-show-process-crypto)))
|
||
|
||
;;;###autoload
|
||
(defun eaf-open-mail-as-html ()
|
||
"Open the html mail in EAF Browser.
|
||
|
||
The value of `mail-user-agent' must be a KEY of the alist `eaf-mua-get-html'.
|
||
|
||
In that way the corresponding function will be called to retrieve the HTML
|
||
part of the current mail."
|
||
(interactive)
|
||
(when-let* ((html (funcall (eaf--get-html-func)))
|
||
(default-directory (eaf--non-remote-default-directory))
|
||
(file (concat (temporary-file-directory) (make-temp-name "eaf-mail-") ".html")))
|
||
(with-temp-file file
|
||
(insert html))
|
||
(eaf-open file "browser" "temp_html_file")))
|
||
|
||
(defun eaf-open-dev-tool-page ()
|
||
(delete-other-windows)
|
||
(split-window (selected-window) (/ (* (nth 3 (eaf-get-window-allocation (selected-window))) 2) 3) nil t)
|
||
(other-window 1)
|
||
(eaf-open "about:blank" "browser" "dev_tools"))
|
||
|
||
;;;###autoload
|
||
(defun eaf-open-browser (url &optional args)
|
||
"Open EAF browser application given a URL and ARGS."
|
||
(interactive "M[EAF/browser] URL: ")
|
||
(eaf-open (eaf-wrap-url url) "browser" args))
|
||
|
||
(defun eaf-browser--duplicate-page-in-new-tab (url)
|
||
"Duplicate a new tab for the dedicated URL."
|
||
(eaf-open (eaf-wrap-url url) "browser" nil t))
|
||
|
||
(defun eaf-is-valid-url (url)
|
||
"Return the same URL if it is valid."
|
||
(when (and url
|
||
;; URL should not include blank char.
|
||
(< (length (split-string url)) 2)
|
||
;; Use regexp matching URL.
|
||
(or (and
|
||
(string-prefix-p "file://" url)
|
||
(string-suffix-p ".html" url))
|
||
;; Normal url address.
|
||
(string-match "^\\(https?://\\)?[a-z0-9]+\\([-.][a-z0-9]+\\)*.+\\..+[a-z0-9.]\\{1,6\\}\\(:[0-9]{1,5}\\)?\\(/.*\\)?$" url)
|
||
;; Localhost url.
|
||
(string-match "^\\(https?://\\)?\\(localhost\\|127.0.0.1\\):[0-9]+/?" url)))
|
||
url))
|
||
|
||
(defun eaf-wrap-url (url)
|
||
"Wraps URL with prefix http:// if URL does not include it."
|
||
(if (or (string-prefix-p "http://" url)
|
||
(string-prefix-p "https://" url)
|
||
(string-prefix-p "file://" url)
|
||
(string-prefix-p "chrome://" url))
|
||
url
|
||
(concat "http://" url)))
|
||
|
||
(defun eaf-goto-left-tab ()
|
||
"Go to left tab when awesome-tab exists."
|
||
(interactive)
|
||
(when (ignore-errors (require 'awesome-tab))
|
||
(awesome-tab-backward-tab)))
|
||
|
||
(defun eaf-goto-right-tab ()
|
||
"Go to right tab when awesome-tab exists."
|
||
(interactive)
|
||
(when (ignore-errors (require 'awesome-tab))
|
||
(awesome-tab-forward-tab)))
|
||
|
||
;;;###autoload
|
||
(defun eaf-open-browser-in-background (url &optional args)
|
||
"Open browser with the specified URL and optional ARGS in background."
|
||
(setq eaf--monitor-configuration-p nil)
|
||
(let ((save-buffer (current-buffer)))
|
||
(eaf-open-browser url args)
|
||
(switch-to-buffer save-buffer))
|
||
(setq eaf--monitor-configuration-p t))
|
||
|
||
;;;###autoload
|
||
(defun eaf-open-browser-with-history ()
|
||
"A wrapper around `eaf-open-browser' that provides browser history candidates.
|
||
|
||
If URL is an invalid URL, it will use `eaf-browser-default-search-engine' to search URL as string literal.
|
||
|
||
This function works best if paired with a fuzzy search package."
|
||
(interactive)
|
||
(let* ((browser-history-file-path
|
||
(concat eaf-config-location
|
||
(file-name-as-directory "browser")
|
||
(file-name-as-directory "history")
|
||
"log.txt"))
|
||
(history-pattern "^\\(.+\\)ᛝ\\(.+\\)ᛡ\\(.+\\)$")
|
||
(history-file-exists (file-exists-p browser-history-file-path))
|
||
(history (completing-read
|
||
"[EAF/browser] Search || URL || History: "
|
||
(if history-file-exists
|
||
(mapcar
|
||
(lambda (h) (when (string-match history-pattern h)
|
||
(format "[%s] ⇰ %s" (match-string 1 h) (match-string 2 h))))
|
||
(with-temp-buffer (insert-file-contents browser-history-file-path)
|
||
(split-string (buffer-string) "\n" t)))
|
||
nil)))
|
||
(history-url (eaf-is-valid-url (when (string-match "⇰\s\\(.+\\)$" history)
|
||
(match-string 1 history)))))
|
||
(cond (history-url (eaf-open-browser history-url))
|
||
((eaf-is-valid-url history) (eaf-open-browser history))
|
||
(t (eaf-search-it history)))))
|
||
|
||
;;;###autoload
|
||
(defun eaf-search-it (&optional search-string search-engine)
|
||
"Use SEARCH-ENGINE search SEARCH-STRING.
|
||
|
||
If called interactively, SEARCH-STRING is defaulted to symbol or region string.
|
||
The user can enter a customized SEARCH-STRING. SEARCH-ENGINE is defaulted
|
||
to `eaf-browser-default-search-engine' with a prefix arg, the user is able to
|
||
choose a search engine defined in `eaf-browser-search-engines'"
|
||
(interactive)
|
||
(let* ((real-search-engine (if current-prefix-arg
|
||
(let ((all-search-engine (mapcar #'car eaf-browser-search-engines)))
|
||
(completing-read
|
||
(format "[EAF/browser] Select search engine (default %s): " eaf-browser-default-search-engine)
|
||
all-search-engine nil t nil nil eaf-browser-default-search-engine))
|
||
(or search-engine eaf-browser-default-search-engine)))
|
||
(link (or (cdr (assoc real-search-engine
|
||
eaf-browser-search-engines))
|
||
(error (format "[EAF/browser] Search engine %s is unknown to EAF!" real-search-engine))))
|
||
(current-symbol (if mark-active
|
||
(if (eq major-mode 'pdf-view-mode)
|
||
(car (pdf-view-active-region-text))
|
||
(buffer-substring (region-beginning) (region-end)))
|
||
(symbol-at-point)))
|
||
(search-url (if search-string
|
||
(format link search-string)
|
||
(let ((search-string (read-string (format "[EAF/browser] Search (%s): " current-symbol))))
|
||
(if (string-blank-p search-string)
|
||
(format link current-symbol)
|
||
(format link search-string))))))
|
||
(eaf-open search-url "browser")))
|
||
|
||
;;;###autoload
|
||
(define-obsolete-function-alias 'eaf-open-url #'eaf-open-browser "20191224")
|
||
|
||
;;;###autoload
|
||
(defun eaf-open-demo ()
|
||
"Open EAF demo screen to verify that EAF is working properly."
|
||
(interactive)
|
||
(eaf-open "eaf-demo" "demo"))
|
||
|
||
;;;###autoload
|
||
(defun eaf-open-camera ()
|
||
"Open EAF camera application."
|
||
(interactive)
|
||
(eaf-open "eaf-camera" "camera"))
|
||
|
||
(defun eaf-open-ipython ()
|
||
"Open ipython in terminal."
|
||
(interactive)
|
||
(if (executable-find "ipython")
|
||
(eaf-terminal-run-command-in-dir "ipython" (eaf--non-remote-default-directory))
|
||
(message "[EAF/terminal] Please install ipython first.")))
|
||
|
||
(defun eaf-open-jupyter ()
|
||
"Open jupyter."
|
||
(interactive)
|
||
(if (executable-find "jupyter-qtconsole")
|
||
(let* ((data (json-read-from-string (shell-command-to-string "jupyter kernelspec list --json")))
|
||
(kernel (completing-read "Jupyter Kernels: " (mapcar #'car (alist-get 'kernelspecs data))))
|
||
(args (make-hash-table :test 'equal)))
|
||
(puthash "kernel" kernel args)
|
||
(eaf-open (format "eaf-jupyter-%s" kernel) "jupyter" (json-encode-hash-table args) t))
|
||
(message "[EAF/jupyter] Please install qtconsole first.")))
|
||
|
||
;;;###autoload
|
||
(defun eaf-open-terminal ()
|
||
"Open EAF Terminal, a powerful GUI terminal emulator in Emacs.
|
||
|
||
The initial directory is `default-directory'. However, it opens `$HOME'
|
||
when `default-directory' is part of a remote process.
|
||
|
||
If a buffer of EAF Terminal in `default-directory' exists, switch to the buffer.
|
||
To override and open a new terminal regardless, call interactively with prefix arg."
|
||
(interactive)
|
||
(eaf-terminal-run-command-in-dir (eaf--generate-terminal-command) (eaf--non-remote-default-directory) t))
|
||
|
||
(defun eaf-terminal-run-command-in-dir (command dir &optional always-new)
|
||
"Run COMMAND in terminal in directory DIR.
|
||
|
||
If ALWAYS-NEW is non-nil, always open a new terminal for the dedicated DIR."
|
||
(let ((args (make-hash-table :test 'equal)))
|
||
(puthash "command" command args)
|
||
(puthash "directory" (expand-file-name dir) args)
|
||
(eaf-open dir "terminal" (json-encode-hash-table args) always-new)))
|
||
|
||
(defun eaf--non-remote-default-directory ()
|
||
"Return `default-directory' itself if is not part of remote, otherwise return $HOME."
|
||
(if (or (file-remote-p default-directory)
|
||
(not (file-exists-p default-directory)))
|
||
(getenv "HOME")
|
||
default-directory))
|
||
|
||
(defun eaf--generate-terminal-command ()
|
||
(getenv "SHELL"))
|
||
|
||
(defun eaf--get-app-for-extension (extension-name)
|
||
"Given the EXTENSION-NAME, loops through `eaf-app-extensions-alist', set and return `app-name'."
|
||
(let ((app-name
|
||
(cl-loop for (app . ext) in eaf-app-extensions-alist
|
||
if (member extension-name (symbol-value ext))
|
||
return app)))
|
||
(if (string-equal app-name "video-player")
|
||
;; Use Browser play video if QWebEngine include private codec.
|
||
(if (string-equal eaf--webengine-include-private-codec "True") "js-video-player" "video-player")
|
||
app-name)))
|
||
|
||
;;;###autoload
|
||
(defun eaf-get-file-name-extension (file)
|
||
"A wrapper around `file-name-extension' that downcases the extension of the FILE."
|
||
(downcase (file-name-extension file)))
|
||
|
||
;;;###autoload
|
||
(defun eaf-open (url &optional app-name args always-new)
|
||
"Open an EAF application with URL, optional APP-NAME and ARGS.
|
||
|
||
Interactively, a prefix arg replaces ALWAYS-NEW, which means to open a new
|
||
buffer regardless of whether a buffer with existing URL and APP-NAME exists.
|
||
|
||
By default, `eaf-open' will switch to buffer if corresponding url exists.
|
||
`eaf-open' always open new buffer if option OPEN-ALWAYS is non-nil.
|
||
|
||
When called interactively, URL accepts a file that can be opened by EAF."
|
||
(interactive "F[EAF] EAF Open: ")
|
||
;; Try to set app-name along with url when calling INTERACTIVELY
|
||
(when (and (not app-name) (file-exists-p url))
|
||
(setq url (expand-file-name url))
|
||
(when (featurep 'recentf)
|
||
(recentf-add-file url))
|
||
(let* ((extension-name (eaf-get-file-name-extension url)))
|
||
;; Initialize url, app-name and args
|
||
(setq app-name (eaf--get-app-for-extension extension-name))
|
||
(cond
|
||
((equal app-name "browser")
|
||
(setq url (concat "file://" url)))
|
||
((equal app-name "office")
|
||
(user-error "Please use `eaf-open-office' instead!")))))
|
||
;; Now that app-name should hopefully be set
|
||
(unless app-name
|
||
;; Output error to user if app-name is empty string.
|
||
(user-error (concat (if app-name (concat "[EAF/" app-name "] ") "[EAF] ")
|
||
(cond
|
||
((not (or (string-prefix-p "/" url)
|
||
(string-prefix-p "~" url))) "File %s cannot be opened.")
|
||
((file-exists-p url) "File %s cannot be opened.")
|
||
(t "File %s does not exist.")))
|
||
url))
|
||
(unless args (setq args ""))
|
||
(setq always-new (or always-new current-prefix-arg))
|
||
;; Hooks are only added if not present already...
|
||
(add-hook 'window-size-change-functions #'eaf-monitor-window-size-change)
|
||
(add-hook 'window-configuration-change-hook #'eaf-monitor-configuration-change)
|
||
;; Open URL with EAF application
|
||
(if (epc:live-p eaf-epc-process)
|
||
(let (exists-eaf-buffer)
|
||
;; Try to open buffer
|
||
(catch 'found-eaf
|
||
(eaf-for-each-eaf-buffer
|
||
(when (and (string= eaf--buffer-url url)
|
||
(string= eaf--buffer-app-name app-name))
|
||
(setq exists-eaf-buffer buffer)
|
||
(throw 'found-eaf t))))
|
||
;; Switch to existing buffer,
|
||
;; if no match buffer found, call `eaf--open-internal'.
|
||
(if (and exists-eaf-buffer
|
||
(not always-new))
|
||
(progn
|
||
(eaf--display-app-buffer app-name exists-eaf-buffer)
|
||
(message (concat "[EAF/" app-name "] " "Switch to %s") url))
|
||
(eaf--open-internal url app-name args)
|
||
(message (concat "[EAF/" app-name "] " "Opening %s") url)))
|
||
;; Record user input, and call `eaf--open-internal' after receive `start_finish' signal from server process.
|
||
(unless eaf--active-buffers
|
||
(push `(,url ,app-name ,args) eaf--active-buffers))
|
||
(eaf-start-process)
|
||
(message (concat "[EAF/" app-name "] " "Opening %s") url)))
|
||
|
||
(defun eaf--display-app-buffer (app-name buffer)
|
||
"Display specified APP-NAME's app buffer in BUFFER."
|
||
(let ((display-fun (or (cdr (assoc app-name
|
||
eaf-app-display-function-alist))
|
||
#'switch-to-buffer)))
|
||
(funcall display-fun buffer)))
|
||
|
||
(defun eaf-split-preview-windows (url)
|
||
"Function for spliting preview windows with specified URL."
|
||
(delete-other-windows)
|
||
(find-file url)
|
||
(split-window-horizontally)
|
||
(other-window +1))
|
||
|
||
(defun eaf-open-airshare ()
|
||
"Open EAF Airshare application, share text string with your phone."
|
||
(interactive)
|
||
(let* ((current-symbol (if (use-region-p)
|
||
(buffer-substring-no-properties (region-beginning) (region-end))
|
||
(thing-at-point 'symbol)))
|
||
(input-string (string-trim (read-string (format "[EAF/airshare] Share Text (%s): " current-symbol)))))
|
||
(when (string-empty-p input-string)
|
||
(setq input-string current-symbol))
|
||
(eaf-open input-string "airshare")))
|
||
|
||
(define-obsolete-function-alias 'eaf-file-transfer-airshare #'eaf-open-airshare "20191224")
|
||
|
||
(defun eaf-file-sender-qrcode (file)
|
||
"Open EAF File Sender application.
|
||
|
||
Select the file FILE to send to your smartphone, a QR code for the corresponding file will appear.
|
||
|
||
Make sure that your smartphone is connected to the same WiFi network as this computer."
|
||
(interactive "F[EAF/file-sender] Select File: ")
|
||
(eaf-open file "file-sender"))
|
||
|
||
(defun eaf-file-sender-qrcode-in-dired ()
|
||
"Open EAF File Transfer application using `eaf-file-sender-qrcode' on
|
||
the file at current cursor position in dired."
|
||
(interactive)
|
||
(eaf-file-sender-qrcode (dired-get-filename)))
|
||
|
||
(defun eaf-file-browser-qrcode (dir)
|
||
"Open EAF File Browser application.
|
||
|
||
Select directory DIR to share file from the smartphone.
|
||
|
||
Make sure that your smartphone is connected to the same WiFi network as this computer."
|
||
(interactive "D[EAF/file-browser] Specify Destination: ")
|
||
(eaf-open dir "file-browser"))
|
||
|
||
(defun eaf-edit-buffer-cancel ()
|
||
"Cancel EAF Browser focus text input and closes the buffer."
|
||
(interactive)
|
||
(kill-buffer)
|
||
(delete-window)
|
||
(message "[EAF/%s] Edit cancelled!" eaf--buffer-app-name))
|
||
|
||
(defun eaf-edit-buffer-confirm ()
|
||
"Confirm input text and send the text to corresponding EAF app."
|
||
(interactive)
|
||
;; Note: pickup buffer-id from buffer name and not restore buffer-id from buffer local variable.
|
||
;; Then we can switch edit buffer to any other mode, such as org-mode, to confirm buffer string.
|
||
(cond ((equal eaf-mindmap--current-add-mode "sub")
|
||
(eaf-call-async "update_multiple_sub_nodes"
|
||
eaf--buffer-id
|
||
(buffer-string)))
|
||
((equal eaf-mindmap--current-add-mode "brother")
|
||
(eaf-call-async "update_multiple_brother_nodes"
|
||
eaf--buffer-id
|
||
(buffer-string)))
|
||
((equal eaf-mindmap--current-add-mode "middle")
|
||
(eaf-call-async "update_multiple_middle_nodes"
|
||
eaf--buffer-id
|
||
(buffer-string)))
|
||
(t
|
||
(eaf-call-async "update_focus_text"
|
||
eaf--buffer-id
|
||
(base64-encode-string (encode-coding-string (buffer-string) 'utf-8)))))
|
||
(kill-buffer)
|
||
(delete-window))
|
||
|
||
(defun eaf-edit-buffer-switch-to-org-mode ()
|
||
"Switch to org-mode to edit table handly."
|
||
(interactive)
|
||
(let ((buffer-app-name eaf--buffer-app-name)
|
||
(buffer-id eaf--buffer-id))
|
||
(org-mode)
|
||
(set (make-local-variable 'eaf--buffer-app-name) buffer-app-name)
|
||
(set (make-local-variable 'eaf--buffer-id) buffer-id)
|
||
(outline-show-all)
|
||
(beginning-of-buffer)
|
||
(local-set-key (kbd "C-c C-c") 'eaf-edit-buffer-confirm)
|
||
(local-set-key (kbd "C-c C-k") 'eaf-edit-buffer-cancel)
|
||
(eaf--edit-set-header-line)))
|
||
|
||
(defun eaf-create-mindmap ()
|
||
"Create a new Mindmap file."
|
||
(interactive)
|
||
(eaf-open " " "mindmap"))
|
||
|
||
(defun eaf-open-mindmap (file)
|
||
"Open a given Mindmap FILE."
|
||
(interactive "f[EAF/mindmap] Select Mindmap file: ")
|
||
(eaf-open file "mindmap"))
|
||
|
||
(defun eaf-get-file-md5 (file)
|
||
"Get the MD5 value of a specified FILE."
|
||
(car (split-string (shell-command-to-string (format "md5sum '%s'" (file-truename file))) " ")))
|
||
|
||
(defun eaf-open-office (file)
|
||
"View Microsoft Office FILE as READ-ONLY PDF."
|
||
(interactive "f[EAF/office] Open Office file as PDF: ")
|
||
(if (executable-find "libreoffice")
|
||
(let* ((file-md5 (eaf-get-file-md5 file))
|
||
(file-name-base (file-name-base file))
|
||
(convert-file (format "/tmp/%s.pdf" file-name-base))
|
||
(pdf-file (format "/tmp/%s.pdf" file-md5)))
|
||
(if (file-exists-p pdf-file)
|
||
(eaf-open pdf-file "pdf-viewer" (concat file-name-base "_office-pdf"))
|
||
(message "Converting %s to PDF format, EAF will start after convert finish." file)
|
||
(make-process
|
||
:name ""
|
||
:buffer " *eaf-open-office*"
|
||
:command (list "libreoffice" "--headless" "--convert-to" "pdf" (file-truename file) "--outdir" "/tmp")
|
||
:sentinel (lambda (process event)
|
||
(when (string= (substring event 0 -1) "finished")
|
||
(rename-file convert-file pdf-file)
|
||
(eaf-open pdf-file "pdf-viewer" (concat file-name-base "_office-pdf")))))))
|
||
(error "[EAF/office] libreoffice is required convert Office file to PDF!")))
|
||
|
||
(defun eaf--atomic-edit (buffer-id focus-text)
|
||
"EAF Browser: edit FOCUS-TEXT with Emacs's BUFFER-ID."
|
||
(split-window-below -10)
|
||
(other-window 1)
|
||
(let ((edit-text-buffer (generate-new-buffer (format "eaf-%s-atomic-edit" eaf--buffer-app-name)))
|
||
(buffer-app-name eaf--buffer-app-name))
|
||
(with-current-buffer edit-text-buffer
|
||
(eaf-edit-mode)
|
||
(set (make-local-variable 'eaf--buffer-app-name) buffer-app-name)
|
||
(set (make-local-variable 'eaf--buffer-id) buffer-id))
|
||
(switch-to-buffer edit-text-buffer)
|
||
(setq-local eaf-mindmap--current-add-mode "")
|
||
(eaf--edit-set-header-line)
|
||
(insert (base64-decode-string focus-text))
|
||
;; When text line number above
|
||
(when (> (line-number-at-pos) 30)
|
||
(beginning-of-buffer))
|
||
))
|
||
|
||
(defun eaf--edit-set-header-line ()
|
||
"Set header line."
|
||
(setq header-line-format
|
||
(substitute-command-keys
|
||
(concat
|
||
"\\<eaf-edit-mode-map>"
|
||
" EAF/" eaf--buffer-app-name " EDIT: "
|
||
"Confirm with `\\[eaf-edit-buffer-confirm]', "
|
||
"Cancel with `\\[eaf-edit-buffer-cancel]'. "
|
||
"Switch to org-mode with `\\[eaf-edit-buffer-switch-to-org-mode]'. "
|
||
))))
|
||
|
||
(defun eaf--enter-fullscreen-request ()
|
||
"Entering EAF browser fullscreen use Emacs frame's size."
|
||
(setq-local eaf-fullscreen-p t)
|
||
(eaf-monitor-configuration-change))
|
||
|
||
(defun eaf--exit_fullscreen_request ()
|
||
"Exit EAF browser fullscreen."
|
||
(setq-local eaf-fullscreen-p nil)
|
||
(eaf-monitor-configuration-change))
|
||
|
||
(defun eaf-browser-send-esc-or-exit-fullscreen ()
|
||
"Escape fullscreen status if browser current is fullscreen.
|
||
Otherwise send key 'esc' to browser."
|
||
(interactive)
|
||
(if eaf-fullscreen-p
|
||
(eaf-call-async "execute_function" eaf--buffer-id "exit_fullscreen" "<escape>")
|
||
(eaf-call-async "send_key" eaf--buffer-id "<escape>")))
|
||
|
||
;; Update and load the theme
|
||
(defun eaf-get-theme-mode ()
|
||
(format "%s"(frame-parameter nil 'background-mode)))
|
||
|
||
(defun eaf-get-theme-background-color ()
|
||
(format "%s"(frame-parameter nil 'background-color)))
|
||
|
||
(defun eaf-get-theme-foreground-color ()
|
||
(format "%s"(frame-parameter nil 'foreground-color)))
|
||
|
||
(eaf-setq eaf-emacs-theme-mode (eaf-get-theme-mode))
|
||
|
||
(eaf-setq eaf-emacs-theme-background-color (eaf-get-theme-background-color))
|
||
|
||
(eaf-setq eaf-emacs-theme-foreground-color (eaf-get-theme-foreground-color))
|
||
|
||
(advice-add 'load-theme :around #'eaf-monitor-load-theme)
|
||
(defun eaf-monitor-load-theme (orig-fun &optional arg &rest args)
|
||
"Update `eaf-emacs-theme-mode' after execute `load-theme'."
|
||
(apply orig-fun arg args)
|
||
(eaf-setq eaf-emacs-theme-mode (eaf-get-theme-mode))
|
||
(eaf-setq eaf-emacs-theme-background-color (eaf-get-theme-background-color))
|
||
(eaf-setq eaf-emacs-theme-foreground-color (eaf-get-theme-foreground-color)))
|
||
|
||
(define-minor-mode eaf-pdf-outline-mode
|
||
"EAF pdf outline mode."
|
||
:keymap (let ((map (make-sparse-keymap)))
|
||
(define-key map (kbd "RET") 'eaf-pdf-outline-jump)
|
||
(define-key map (kbd "q") 'quit-window)
|
||
map))
|
||
|
||
(defun eaf-pdf-outline ()
|
||
"Create PDF outline."
|
||
(interactive)
|
||
(let ((buffer-name (buffer-name (current-buffer)))
|
||
(toc (eaf-call-sync "call_function" eaf--buffer-id "get_toc"))
|
||
(page-number (string-to-number (eaf-call-sync "call_function" eaf--buffer-id "current_page"))))
|
||
;; Save window configuration before outline.
|
||
(setq eaf-pdf-outline-window-configuration (current-window-configuration))
|
||
|
||
;; Insert outline content.
|
||
(with-current-buffer (get-buffer-create eaf-pdf-outline-buffer-name)
|
||
(setq buffer-read-only nil)
|
||
(erase-buffer)
|
||
(insert toc)
|
||
(setq toc (mapcar (lambda (line)
|
||
(string-to-number (car (last (split-string line " ")))))
|
||
(butlast (split-string (buffer-string) "\n"))))
|
||
(goto-line (seq-count (apply-partially #'>= page-number) toc))
|
||
(set (make-local-variable 'eaf-pdf-outline-original-buffer-name) buffer-name)
|
||
(let ((view-read-only nil))
|
||
(read-only-mode 1))
|
||
(eaf-pdf-outline-mode 1))
|
||
|
||
;; Popup ouline buffer.
|
||
(pop-to-buffer eaf-pdf-outline-buffer-name)))
|
||
|
||
(defun eaf-pdf-outline-jump ()
|
||
"Jump into specific page."
|
||
(interactive)
|
||
(let* ((line (thing-at-point 'line))
|
||
(page-num (replace-regexp-in-string "\n" "" (car (last (s-split " " line))))))
|
||
;; Jump to page.
|
||
(switch-to-buffer-other-window eaf-pdf-outline-original-buffer-name)
|
||
(eaf-call-sync "call_function_with_args" eaf--buffer-id "jump_to_page_with_num" (format "%s" page-num))
|
||
|
||
;; Restore window configuration before outline operation.
|
||
(when eaf-pdf-outline-window-configuration
|
||
(set-window-configuration eaf-pdf-outline-window-configuration)
|
||
(setq eaf-pdf-outline-window-configuration nil))))
|
||
|
||
(defun eaf-pdf-get-annots (page)
|
||
"Return a map of annotations on PAGE.
|
||
|
||
The key is the annot id on PAGE."
|
||
(eaf-call-sync "call_function_with_args" eaf--buffer-id "get_annots" (format "%s" page)))
|
||
|
||
(defun eaf-pdf-jump-to-annot (annot)
|
||
"Jump to specifical pdf annot."
|
||
(let ((rect (gethash "rect" annot))
|
||
(page (gethash "page" annot)))
|
||
(eaf-call-sync "call_function_with_args" eaf--buffer-id "jump_to_rect" (format "%s" page) rect)))
|
||
|
||
(defun eaf--get-current-desktop-name ()
|
||
"Get current desktop name by `wmctrl'."
|
||
(if (string-empty-p eaf-wm-name)
|
||
(if (executable-find "wmctrl")
|
||
;; Get desktop name by command `wmctrl -m'.
|
||
(cl-second (split-string (cl-first (split-string (shell-command-to-string "wmctrl -m") "\n")) ": "))
|
||
;; Otherwise notify user and return emptry string.
|
||
(message "You need install wmctrl to get the name of desktop.")
|
||
"")
|
||
eaf-wm-name))
|
||
|
||
(defun eaf--activate-emacs-win32-window()
|
||
"Use vbs activate emacs win32 window."
|
||
(let* ((activate-window-file-path
|
||
(concat eaf-config-location "activate-window.vbs"))
|
||
(activate-window-file-exists (file-exists-p activate-window-file-path)))
|
||
(unless activate-window-file-exists
|
||
(with-temp-file activate-window-file-path
|
||
(insert "set WshShell = CreateObject(\"WScript.Shell\")\nWshShell.AppActivate Wscript.Arguments(0)")))
|
||
(shell-command-to-string (format "cscript %s %s" activate-window-file-path (emacs-pid)))))
|
||
|
||
(defun eaf--activate-emacs-linux-window ()
|
||
"Activate emacs window by `wmctrl'."
|
||
(if (member (eaf--get-current-desktop-name) eaf-wm-focus-fix-wms)
|
||
;; When switch app focus in WM, such as, i3 or qtile.
|
||
;; Emacs window cannot get the focus normally if mouse in EAF buffer area.
|
||
;;
|
||
;; So we move mouse to frame bottom of Emacs, to make EAF receive input event.
|
||
(if (executable-find "xdotool")
|
||
(shell-command-to-string (format "xdotool mousemove %d %d" (car (frame-edges)) (nth 3 (frame-edges))))
|
||
(message "Please install xdotool to make mouse to frame bottom automatically."))
|
||
|
||
;; When press Alt + Tab in DE, such as KDE.
|
||
;; Emacs window cannot get the focus normally if mouse in EAF buffer area.
|
||
;;
|
||
;; So we use wmctrl activate on Emacs window after Alt + Tab operation.
|
||
(if (executable-find "wmctrl")
|
||
(shell-command-to-string (format "wmctrl -i -a $(wmctrl -lp | awk -vpid=$PID '$3==%s {print $1; exit}')" (emacs-pid)))
|
||
(message "Please install wmctrl to active emacs window."))))
|
||
|
||
(defun eaf-activate-emacs-window()
|
||
"Activate emacs window."
|
||
(if (memq system-type '(cygwin windows-nt ms-dos))
|
||
(eaf--activate-emacs-win32-window)
|
||
(eaf--activate-emacs-linux-window)))
|
||
|
||
(defun eaf-elfeed-open-url ()
|
||
"Display the currently selected item in an eaf buffer."
|
||
(interactive)
|
||
(if (featurep 'elfeed)
|
||
(let ((entry (elfeed-search-selected :ignore-region)))
|
||
(require 'elfeed-show)
|
||
(when (elfeed-entry-p entry)
|
||
;; Move to next feed item.
|
||
(elfeed-untag entry 'unread)
|
||
(elfeed-search-update-entry entry)
|
||
(unless elfeed-search-remain-on-entry (forward-line))
|
||
|
||
;; Open elfeed item in other window,
|
||
;; and scroll EAF browser content by command `scroll-other-window'.
|
||
(delete-other-windows)
|
||
(pcase eaf-elfeed-split-direction
|
||
("below"
|
||
(split-window-no-error nil 30 'up)
|
||
(eaf--select-window-by-direction "down")
|
||
(eaf-open-browser (elfeed-entry-link entry))
|
||
(eaf--select-window-by-direction "up"))
|
||
("right"
|
||
(split-window-no-error nil 60 'right)
|
||
(eaf--select-window-by-direction "right")
|
||
(eaf-open-browser (elfeed-entry-link entry))
|
||
(eaf--select-window-by-direction "left")))
|
||
))
|
||
(message "Please install elfeed first.")))
|
||
|
||
(defun eaf--select-window-by-direction (direction)
|
||
"Select the most on the side according to the direction."
|
||
(ignore-errors
|
||
(dotimes (i 50)
|
||
(pcase direction
|
||
("left" (windmove-left))
|
||
("right" (windmove-right))
|
||
("up" (windmove-up))
|
||
("below" (windmove-down))
|
||
))))
|
||
|
||
(defun eaf--change-default-directory (directory)
|
||
"Change default directory to DIRECTORY."
|
||
(when (file-accessible-directory-p (file-name-directory directory))
|
||
(setq-local default-directory directory)))
|
||
|
||
;;;;;;;;;;;;;;;;;;;; Utils ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||
(defun eaf-get-view-info ()
|
||
(let* ((window-allocation (eaf-get-window-allocation (selected-window)))
|
||
(x (nth 0 window-allocation))
|
||
(y (nth 1 window-allocation))
|
||
(w (nth 2 window-allocation))
|
||
(h (nth 3 window-allocation)))
|
||
(format "%s:%s:%s:%s:%s" eaf--buffer-id x y w h)))
|
||
|
||
(defun eaf-generate-keymap-doc ()
|
||
"This command use for generate keybindings document Wiki."
|
||
(interactive)
|
||
(let ((vars (list 'eaf-browser-keybinding
|
||
'eaf-browser-caret-mode-keybinding
|
||
'eaf-pdf-viewer-keybinding
|
||
'eaf-video-player-keybinding
|
||
'eaf-js-video-player-keybinding
|
||
'eaf-image-viewer-keybinding
|
||
'eaf-terminal-keybinding
|
||
'eaf-camera-keybinding
|
||
'eaf-mindmap-keybinding
|
||
'eaf-jupyter-keybinding
|
||
)))
|
||
(erase-buffer)
|
||
(insert "**** Entire document automatically generated by command =eaf-generate-keymap-doc=.\n\n")
|
||
(insert "* Overview
|
||
Each EAF App has its own set of keybindings. Their default bindings are listed below. You can also see this list by executing =(describe-mode)= or =C-h m= within an EAF buffer.
|
||
|
||
You can customize them very easily with the =eaf-bind-key= function: find the corresponding *Keybinding Variable*, and add the something similar to the following to =.emacs=
|
||
#+BEGIN_SRC emacs-lisp
|
||
(eaf-bind-key scroll_up \"C-n\" eaf-pdf-viewer-keybinding)
|
||
#+END_SRC
|
||
To *unbind* an existing keybinding, use the following:
|
||
#+begin_src emacs-lisp
|
||
(eaf-bind-key nil \"C-n\" eaf-pdf-viewer-keybinding)
|
||
#+end_src
|
||
|
||
* Global keybindings
|
||
| Key | Event |
|
||
|-------+-----------------------------|
|
||
| C-h m | eaf-describe-bindings |
|
||
| C-c b | eaf-open-bookmark |
|
||
| C-c e | eaf-open-external |
|
||
| C-c i | eaf-import-chrome-bookmarks |
|
||
| M-/ | eaf-get-path-or-url |
|
||
| M-' | eaf-toggle-fullscreen |
|
||
| M-[ | eaf-share-path-or-url |
|
||
|
||
* Browser Edit Mode
|
||
| Key | Event |
|
||
|---------+------------------------------------|
|
||
| C-c C-c | eaf-edit-buffer-confirm |
|
||
| C-c C-k | eaf-edit-buffer-cancel |
|
||
| C-c C-t | eaf-edit-buffer-switch-to-org-mode |
|
||
|
||
")
|
||
(dolist (var vars)
|
||
(insert (format "* %s\n" (get var 'variable-documentation)))
|
||
(insert (format " *Keybinding Variable*: =%s=\n" (symbol-name var)))
|
||
(insert "| Key | Event |\n")
|
||
(insert "|-----+------|\n")
|
||
;; NOTE: `standard-value' use for fetch origin value of keybinding variable.
|
||
;; Otherwise, developer's personal config will dirty document.
|
||
(dolist (element (eval (car (get var 'standard-value))))
|
||
(insert (format "| %s | %s |\n" (car element) (cdr element))))
|
||
(insert "\n"))))
|
||
|
||
;;;;;;;;;;;;;;;;;;;; Advice ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||
|
||
;; FIXME: In the code below we should use `save-selected-window' (or even
|
||
;; better `with-selected-window') rather than (other-window +1) followed by
|
||
;; (other-window -1) since this is not always a no-op.
|
||
|
||
(advice-add 'scroll-other-window :around #'eaf--scroll-other-window)
|
||
(defun eaf--scroll-other-window (orig-fun &optional arg &rest args)
|
||
"When next buffer is `eaf-mode', do `eaf-scroll-up-or-next-page'."
|
||
(other-window +1)
|
||
(if (derived-mode-p 'eaf-mode)
|
||
(progn
|
||
(eaf-call-async "scroll_other_buffer" (eaf-get-view-info) "up"
|
||
(if arg "line" "page"))
|
||
(other-window -1))
|
||
(other-window -1)
|
||
(apply orig-fun arg args)))
|
||
|
||
(advice-add 'scroll-other-window-down :around #'eaf--scroll-other-window-down)
|
||
(defun eaf--scroll-other-window-down (orig-fun &optional arg &rest args)
|
||
"When next buffer is `eaf-mode', do `eaf-scroll-down-or-previous-page'."
|
||
(other-window +1)
|
||
(if (derived-mode-p 'eaf-mode)
|
||
(progn
|
||
(eaf-call-async "scroll_other_buffer" (eaf-get-view-info) "down"
|
||
(if arg "line" "page"))
|
||
(other-window -1))
|
||
(other-window -1)
|
||
(apply orig-fun arg args)))
|
||
|
||
(advice-add 'watch-other-window-internal :around
|
||
#'eaf--watch-other-window-internal)
|
||
(defun eaf--watch-other-window-internal (orig-fun &optional direction line
|
||
&rest args)
|
||
"When next buffer is `eaf-mode', do `eaf-watch-other-window'."
|
||
(other-window +1)
|
||
(if (derived-mode-p 'eaf-mode)
|
||
(progn
|
||
(eaf-call-async "scroll_other_buffer" (eaf-get-view-info)
|
||
(if (string-equal direction "up") "up" "down")
|
||
(if line "line" "page"))
|
||
(other-window -1))
|
||
(other-window -1)
|
||
(apply orig-fun direction line args)))
|
||
|
||
(defun eaf--find-file-ext-p (ext)
|
||
"Determine file extension EXT can be opened by EAF directly by `find-file'.
|
||
|
||
You can configure a blacklist using `eaf-find-file-ext-blacklist'"
|
||
(and ext
|
||
(member (downcase ext) (append eaf-pdf-extension-list eaf-video-extension-list
|
||
eaf-image-extension-list eaf-mindmap-extension-list))
|
||
(not (member ext eaf-find-file-ext-blacklist))))
|
||
|
||
;; Make EAF as default app for supported extensions.
|
||
;; Use `eaf-open' in `find-file'
|
||
(defun eaf--find-file-advisor (orig-fn file &rest args)
|
||
"Advisor of `find-file' that opens EAF supported file using EAF.
|
||
|
||
It currently identifies PDF, videos, images, and mindmap file extensions."
|
||
(let ((fn (if (commandp 'eaf-open)
|
||
#'(lambda (file)
|
||
(eaf-open file))
|
||
orig-fn))
|
||
(ext (file-name-extension file)))
|
||
(if (eaf--find-file-ext-p ext)
|
||
(apply fn file nil)
|
||
(apply orig-fn file args))))
|
||
(advice-add #'find-file :around #'eaf--find-file-advisor)
|
||
|
||
;; Use `eaf-open' in `dired-find-file' and `dired-find-alternate-file'
|
||
(defun eaf--dired-find-file-advisor (orig-fn)
|
||
"Advisor of `dired-find-file' and `dired-find-alternate-file' that opens EAF supported file using EAF.
|
||
|
||
It currently identifies PDF, videos, images, and mindmap file extensions."
|
||
(dolist (file (dired-get-marked-files))
|
||
(let ((fn (if (commandp 'eaf-open)
|
||
#'(lambda (file)
|
||
(eaf-open file))
|
||
orig-fn))
|
||
(ext (file-name-extension file)))
|
||
(if (eaf--find-file-ext-p ext)
|
||
(apply fn file nil)
|
||
(funcall-interactively orig-fn)))))
|
||
(advice-add #'dired-find-file :around #'eaf--dired-find-file-advisor)
|
||
(advice-add #'dired-find-alternate-file :around #'eaf--dired-find-file-advisor)
|
||
|
||
(provide 'eaf)
|
||
|
||
;;; eaf.el ends here
|