Files
emacs/lisp/emacs-application-framework/eaf.el
2020-12-05 21:29:49 +01:00

2191 lines
81 KiB
EmacsLisp
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
;; 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: Fri Jul 3 02:11:53 2020 (-0400)
;; By: Mingde (Matthew) Zeng
;; URL: http://www.emacswiki.org/emacs/download/eaf.el
;; Keywords:
;; Compatibility: GNU Emacs 27.0.50
;;
;; 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
;;
;;
;;
;;; Require
(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"))))
(require 'dbus)
(require 'subr-x)
(require 'map)
(require 'bookmark)
(require 'seq)
(require 'eaf-mindmap)
(require 'eaf-interleave)
(require 'json)
;;; Code:
;; 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 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.")
(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)
(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 (default-value 'emulation-mode-map-alists))
;; Disable cursor in eaf buffer.
(setq-local cursor-type nil)
(push (list (cons t eaf-mode-map))
emulation-mode-map-alists)
(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-process 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)
(defvar eaf-grip-token nil)
(defvar eaf-find-alternate-file-in-dired nil
"If non-nil, calling `eaf-open-this-from-dired' determines file types to open.
EAF unrecognizable files will be opened by `dired-find-alternate-file' normally.
Otherwise they will be opened normally with `dired-find-file'.")
(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-name "*eaf*"
"Name of EAF buffer."
:type 'string)
(defcustom eaf-python-command "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-var-list
'((eaf-camera-save-path . "~/Downloads")
(eaf-browser-enable-plugin . "true")
(eaf-browser-enable-javascript . "true")
(eaf-browser-remember-history . "true")
(eaf-browser-default-zoom . "1.0")
(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 . "")
(eaf-pdf-dark-mode . "")
(eaf-terminal-dark-mode . "")
(eaf-terminal-font-size . "13")
(eaf-mindmap-dark-mode . "")
(eaf-mindmap-save-path . "~/Documents")
(eaf-marker-letters . "ASDFHJKLWEOPCNM")
(eaf-emacs-theme-mode . ""))
"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-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" . "edit_focus_text")
("M-s" . "open_link")
("M-S" . "open_link_new_buffer")
("M-d" . "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-M-q" . "clear_history")
("M-v" . "scroll_down_page")
("M-<" . "scroll_to_begin")
("M->" . "scroll_to_bottom")
("M-t" . "new_blank_page")
("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")
("D" . "insert_or_open_link_background_buffer")
("c" . "insert_or_copy_link")
("C" . "insert_or_copy_code")
("d" . "insert_or_scroll_up_page")
("u" . "insert_or_scroll_down_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_download_manage_page")
("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")
("s" . "insert_or_save_as_pdf")
("S" . "insert_or_save_as_single_file")
("v" . "insert_or_view_source")
("e" . "insert_or_edit_url")
("C-a" . "select_all_or_input_text")
("M-u" . "clear_focus")
("M-i" . "open_download_manage_page")
("M-o" . "eval_js")
("M-O" . "eval_js_file")
("M-g" . "exit_fullscreen")
("M-," . "eaf-send-down-key")
("M-." . "eaf-send-up-key")
("M-m" . "eaf-send-return-key")
("<f5>" . "refresh_page")
("<f12>" . "open_dev_tool_page")
("<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")
("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")
("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_dev_tool_page")
("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")
("x" . "close_buffer")
("j" . "scroll_up")
("k" . "scroll_down")
("h" . "scroll_left")
("l" . "scroll_right")
("," . "scroll_up_page")
("." . "scroll_down_page")
("<" . "scroll_to_begin")
(">" . "scroll_to_bottom"))
"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-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_dev_tool_page")
("M-w" . "copy_text")
("C-y" . "yank_text")
("C-S-a" . "select_all")
("C-S-l" . "clear_selection")
("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
'(("j" . "take_photo"))
"The keybinding of EAF Camera."
:type 'cons)
(defcustom eaf-rss-reader-keybinding
'(("a" . "add_subscription")
("d" . "delete_subscription")
("n" . "next_subscription")
("p" . "prev_subscription")
("N" . "last_subscription")
("P" . "first_subscription")
("j" . "next_article")
("k" . "prev_article")
("J" . "last_article")
("K" . "first_article")
("x" . "close_buffer")
("," . "scroll_up")
("." . "scroll_down")
("SPC" . "scroll_up_page")
("b" . "scroll_down_page")
("<" . "scroll_to_begin")
(">" . "scroll_to_bottom")
("M-s" . "open_link")
("M-S" . "open_link_new_buffer")
("C-s" . "search_text_forward")
("C-r" . "search_text_backward")
("C-n" . "scroll_up")
("C-p" . "scroll_down")
("C-v" . "scroll_up_page")
("M-v" . "scroll_down_page")
("M-<" . "scroll_to_begin")
("M->" . "scroll_to_bottom"))
"The keybinding of EAF RSS Reader."
: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-j" . "select_down_node")
("M-k" . "select_up_node")
("M-h" . "select_left_node")
("M-l" . "select_right_node")
("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_dev_tool_page")
)
"The keybinding of EAF Mindmap."
:type 'cons)
(defcustom eaf-mermaid-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-w" . "kill_text")
("M-w" . "copy_text")
("M-v" . "scroll_down_page")
("M-<" . "scroll_to_begin")
("M->" . "scroll_to_bottom")
("M-t" . "new_blank_page")
("SPC" . "insert_or_scroll_up_page")
("x" . "insert_or_close_buffer")
("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")
("d" . "insert_or_scroll_up_page")
("u" . "insert_or_scroll_down_page")
("t" . "insert_or_new_blank_page")
("T" . "insert_or_recover_prev_close_page")
("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")
("C-a" . "select_all_or_input_text")
("M-o" . "eval_js")
("M-p" . "eval_js_file")
("<f5>" . "refresh_page")
("<f12>" . "open_dev_tool_page")
)
"The keybinding of EAF Mermaid."
: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-mermaid-extension-list
'("mmd")
"The extension list of mermaid 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" "rmvb" "ogg" "mp4" "mkv")
"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-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-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)
(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)
("rss-reader" . eaf-rss-reader-keybinding)
("mindmap" . eaf-mindmap-keybinding)
("mermaid" . eaf-mermaid-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
'(("mermaid" . eaf--mermaid-preview-display)
("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)
("mermaid" . eaf-mermaid-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' execute.
This variable use 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)
(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 (process-live-p eaf-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--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)))))))
(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-open-external ()
"Command to open current path or url with external application."
(interactive)
(let ((path-or-url (eaf-get-path-or-url)))
(cond ((string-equal system-type "windows-nt")
(w32-shell-execute "open" path-or-url))
((string-equal system-type "darwin")
(concat "open " (shell-quote-argument path-or-url)))
((string-equal system-type "gnu/linux")
(let ((process-connection-type nil))
(start-process "" nil "xdg-open" path-or-url))))))
(defun eaf-call (method &rest args)
"Call EAF Python process using `dbus-call-method' with METHOD and ARGS."
(let ((result (apply #'dbus-call-method
:session ; use the session (not system) bus
"com.lazycat.eaf" ; service name
"/com/lazycat/eaf" ; path name
"com.lazycat.eaf" ; interface name
method
:timeout 1000000
args)))
(cond ((equal result "True") t)
((equal result "False") nil)
(t result))))
(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
((eq eaf--active-buffers nil)
(user-error "[EAF] Please initiate EAF with eaf-open-... functions only"))
((process-live-p eaf-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 eaf-config-location)
(list (eaf-serialization-var-list))
))
(gdb-args (list "-batch" "-ex" "run" "-ex" "bt" "--args" eaf-python-command)))
(setq eaf-process
(if eaf-enable-debug
(apply #'start-process eaf-name eaf-name "gdb" (append gdb-args eaf-args))
(apply #'start-process eaf-name eaf-name eaf-python-command eaf-args))))
(set-process-query-on-exit-flag eaf-process nil)
(set-process-sentinel
eaf-process
#'(lambda (process event)
(when (string-prefix-p "exited abnormally with code" event)
(switch-to-buffer eaf-name))
(message "[EAF] %s %s" process (replace-regexp-in-string "\n$" "" event))))
(message "[EAF] Process starting..."))
(defun eaf-stop-process (&optional restart)
"Stop EAF process and kill all EAF buffers.
When RESTART is non-nil, cached URL and app-name will not be cleared."
(interactive)
;; Kill EAF buffers.
(let ((count 0))
(dolist (buffer (buffer-list))
(set-buffer buffer)
(when (derived-mode-p 'eaf-mode)
(cl-incf count)
(kill-buffer buffer)))
;; Just report to me when EAF buffer exists.
(if (> count 1)
(message "[EAF] Killed %s EAF buffer%s" count (if (> count 1) "s" ""))))
(when (get-buffer eaf-name)
(kill-buffer eaf-name))
;; Clear active buffers
(unless restart
(setq eaf--active-buffers nil))
;; 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 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 (process-live-p eaf-process)
;; Delete EAF server process.
(progn
(delete-process eaf-process)
(message "[EAF] Process terminated."))
(message "[EAF] Process already terminated.")))
(defun eaf-restart-process ()
"Stop and restart EAF process."
(interactive)
(setq eaf--active-buffers nil)
(dolist (buffer (buffer-list))
(set-buffer buffer)
(when (derived-mode-p 'eaf-mode)
(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))
;; Support emacs 27 tab-line-mode.
;; Tab-line-mode news: https://github.com/emacs-mirror/emacs/blob/master/etc/NEWS.27#L2755
(y (+ (nth 1 window-edges)
(window-header-line-height window)
(if (require 'tab-line nil t)
(if tab-line-mode (window-tab-line-height window) 0)
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 "call_function" eaf--buffer-id "get_url")))
(eaf-call "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 "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 "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)
"Configure the `eaf-mode-map' from KEYBINDING, one of the eaf-.*-keybinding variables."
(setq eaf-mode-map
(let ((map (make-sparse-keymap)))
(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 command in python side, build elisp proxy function to call it.
((string-match "_" fun)
(eaf--make-proxy-function fun))
;; If command is string and include - , it's elisp function, use `intern' build elisp function from function name.
((string-match "-" fun)
(intern fun))))
finally return 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)))
(with-current-buffer eaf-buffer
(eaf-mode)
;; `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-is-support (url)
(dbus-call-method
:session "com.lazycat.eaf"
"/com/lazycat/eaf"
"com.lazycat.eaf"
"is_support"
url))
(defun eaf-monitor-window-size-change (frame)
"Update eaf view once emacs FRAME size changed."
(when (process-live-p eaf-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)))))
(defun eaf-try-adjust-view-with-frame-size ()
"Update eaf view once emacs window size changed."
(when (and (equal (frame-pixel-width) eaf-last-frame-width)
(equal (frame-pixel-height) eaf-last-frame-height))
(eaf-monitor-configuration-change)))
(defun eaf-monitor-configuration-change (&rest _)
(when (and eaf--monitor-configuration-p
(process-live-p eaf-process))
(ignore-errors
(let (view-infos)
(dolist (frame (frame-list))
(dolist (window (window-list frame))
(let ((buffer (window-buffer window)))
(with-current-buffer buffer
(if (derived-mode-p 'eaf-mode)
;; Use frame size if just have one window in current frame and `eaf-fullscreen-p' is non-nil.
(if (and (equal (length (window-list)) 1)
eaf-fullscreen-p)
(push (format "%s:%s:%s:%s:%s:%s"
eaf--buffer-id
(eaf-get-emacs-xid frame)
0 0 (frame-pixel-width) (frame-pixel-height))
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)
)))))))
;; I don't know how to make Emacs send dbus-message with two-dimensional list.
;; So I package two-dimensional list in string, then unpack on server side. ;)
(eaf-call "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--monitor-buffer-kill ()
"Function monitoring when an EAF buffer is killed."
(ignore-errors
(eaf-call "kill_buffer" eaf--buffer-id)
(message "[EAF] Killed %s." eaf--buffer-id)))
(defun eaf--monitor-emacs-kill ()
"Function monitoring when Emacs is killed, kill all EAF buffers."
(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 (buffer-list) browser-urls)
(set-buffer buffer)
(when (and (derived-mode-p 'eaf-mode) (equal eaf--buffer-app-name "browser"))
(setq browser-urls (concat eaf--buffer-url "\n" browser-urls))))
nil browser-restore-file-path)))
(eaf-call "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 (process-live-p eaf-process)
(ignore-errors
;; eaf-org-file-list?
(org-html-export-to-html)
(eaf-call "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 "action_quit" eaf--buffer-id)
(call-interactively 'keyboard-quit))
(defun eaf-send-key ()
"Directly send key to EAF Python side."
(interactive)
(eaf-call "send_key" eaf--buffer-id (key-description (this-command-keys-vector))))
(defun eaf-send-down-key ()
"Directly send down key to EAF Python side."
(interactive)
(eaf-call "send_key" eaf--buffer-id "<down>"))
(defun eaf-send-up-key ()
"Directly send up key to EAF Python side."
(interactive)
(eaf-call "send_key" eaf--buffer-id "<up>"))
(defun eaf-send-return-key ()
"Directly send return key to EAF Python side."
(interactive)
(eaf-call "send_key" eaf--buffer-id "RET"))
(defun eaf-send-key-sequence ()
"Directly send key sequence to EAF Python side."
(interactive)
(eaf-call "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 "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 "send_key_sequence" eaf--buffer-id "M-<backspace>"))
(defun eaf-send-second-key-sequence ()
"Send second part of key sequence to terminal."
(interactive)
(eaf-call "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 (process-live-p eaf-process)
;; Update python side variable dynamically.
(eaf-call "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))))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "focus_emacs_buffer"
#'eaf-focus-buffer)
(defun eaf-focus-buffer (focus-buffer-id)
"Focus the buffer given the FOCUS-BUFFER-ID."
(catch 'find-window
(dolist (frame (frame-list))
(dolist (window (window-list frame))
(let ((buffer (window-buffer window)))
(with-current-buffer buffer
(when (and (derived-mode-p 'eaf-mode)
(string= eaf--buffer-id focus-buffer-id)
(select-window window)
(throw 'find-window t)))))))))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "message_to_emacs"
(lambda (format-string) (message (concat "[EAF/" eaf--buffer-app-name "] " format-string))))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "set_emacs_var"
#'eaf--set-emacs-var)
(defun eaf--set-emacs-var (var-name var-value)
"Used by Python applications to set Lisp variable VAR-NAME with value VAR-VALUE on the Emacs side."
(set (intern var-name) var-value))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "eval_in_emacs"
#'eaf--eval-in-emacs)
(defun eaf--eval-in-emacs (elisp-code-string)
"Used by Python applications to evaluate ELISP-CODE-STRING as Emacs Lisp code on the Emacs side."
(eval (read elisp-code-string) 'lexical))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "create_new_browser_buffer"
#'eaf--create-new-browser-buffer)
(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)))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "request_kill_buffer"
#'eaf-request-kill-buffer)
(defun eaf-request-kill-buffer (kill-buffer-id)
"Function for requesting to kill the given buffer with KILL-BUFFER-ID."
(catch 'found-match-buffer
(dolist (buffer (buffer-list))
(set-buffer buffer)
(when (and (derived-mode-p 'eaf-mode)
(string= eaf--buffer-id kill-buffer-id))
(kill-buffer buffer)
(message "[EAF] Request to kill buffer %s." kill-buffer-id)
(throw 'found-match-buffer t)))))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "first_start"
#'eaf--first-start)
(defun eaf--first-start (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."
;; 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")
webengine-include-private-codec)
(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))))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "update_buffer_details"
#'eaf--update-buffer-details)
(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 'find-buffer
(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 'find-buffer t))))))))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "translate_text"
#'eaf-translate-text)
(defun eaf-translate-text (text)
"Use sdcv to translate selected TEXT."
(when (featurep 'sdcv)
(sdcv-search-input+ text)))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "input_message"
#'eaf--input-message)
(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 "handle_input_message" input-buffer-id callback-tag input-message)
(eaf-call "cancel_input_message" 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))
(buffer-result
(with-current-buffer buffer
(eaf-call "new_buffer"
eaf--buffer-id url app-name args))))
(cond ((equal buffer-result "")
(eaf--display-app-buffer app-name buffer))
(t
;; Kill buffer and show error message from python server.
(kill-buffer buffer)
(switch-to-buffer eaf-name)
(message buffer-result))))
(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--mermaid-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)
(markdown-mode))
(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")))
;;;###autoload
(defun eaf-open-rss-reader ()
"Open EAF RSS Reader."
(interactive)
(eaf-open "RSS Reader" "rss-reader"))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "open_dev_tools_page"
#'eaf-open-dev-tool-page)
(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"))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "open_url_in_new_tab"
#'eaf-open-browser)
;;;###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-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))
url
(concat "http://" url)))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "open_url_in_background_tab"
#'eaf-open-browser-in-background)
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "goto_left_tab"
#'eaf-goto-left-tab)
(defun eaf-goto-left-tab ()
"Go to left tab when awesome-tab exists."
(interactive)
(when (ignore-errors (require 'awesome-tab))
(awesome-tab-backward-tab)))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "goto_right_tab"
#'eaf-goto-right-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 0 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
(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)
;;;###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.")))
;;;###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 (file-remote-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 eaf--webengine-include-private-codec "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)))
(defun eaf-open-this-from-dired ()
"Open html/pdf/image/video files whenever possible with EAF in dired.
Other files will open normally with `dired-find-file' or `dired-find-alternate-file'"
(interactive)
(dolist (file (dired-get-marked-files))
(cond
((member (eaf-get-file-name-extension file) eaf-office-extension-list)
(eaf-open-office file))
((eaf--get-app-for-extension
(eaf-get-file-name-extension file))
(eaf-open file))
(eaf-find-alternate-file-in-dired
(dired-find-alternate-file))
(t (dired-find-file)))))
;;;###autoload
(define-obsolete-function-alias 'eaf-file-open-in-dired #'eaf-open-this-from-dired)
;;;###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 "markdown-previewer")
;; Try get user's github token if `eaf-grip-token' is nil.
(setq args
(or eaf-grip-token
(read-string (concat "[EAF/" app-name "] Fill your own Github token (or set `eaf-grip-token' with token string): ")))))
((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 (process-live-p eaf-process)
(let (exists-eaf-buffer)
;; Try to open buffer
(catch 'found-match-buffer
(dolist (buffer (buffer-list))
(set-buffer buffer)
(when (derived-mode-p 'eaf-mode)
(when (and (string= eaf--buffer-url url)
(string= eaf--buffer-app-name app-name))
(setq exists-eaf-buffer buffer)
(throw 'found-match-buffer 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)
(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.
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 EAF Browser focus text input and send the text to EAF Browser."
(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.
(eaf-call "update_focus_text"
(replace-regexp-in-string "eaf-\\(.*?\\)-edit-focus-text-" "" (buffer-name))
(buffer-string))
(kill-buffer)
(delete-window))
(defun eaf-edit-buffer-switch-to-org-mode ()
"Switch to org-mode to edit table handly."
(interactive)
(org-mode)
(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!")))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "edit_focus_text"
#'eaf--edit-focus-text)
(defun eaf--edit-focus-text (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-edit-focus-text-%s" eaf--buffer-app-name buffer-id))))
(switch-to-buffer edit-text-buffer)
(eaf-edit-mode)
(eaf--edit-set-header-line)
(insert 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]'. "
))))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "enter_fullscreen_request"
#'eaf--enter_fullscreen_request)
(defun eaf--enter_fullscreen_request ()
"Entering EAF browser fullscreen use emacs frame's size."
(setq-local eaf-fullscreen-p t)
(eaf-monitor-configuration-change))
(dbus-register-signal
:session "com.lazycat.eaf" "/com/lazycat/eaf"
"com.lazycat.eaf" "exit_fullscreen_request"
#'eaf--exit_fullscreen_request)
(defun eaf--exit_fullscreen_request ()
"Exit EAF browser fullscreen."
(setq-local eaf-fullscreen-p nil)
(eaf-monitor-configuration-change))
(dbus-register-service :session "com.lazycat.emacs")
;; Update and load the theme
(defun eaf-get-theme-mode ()
(format "%s"(frame-parameter nil 'background-mode)))
(eaf-setq eaf-emacs-theme-mode (eaf-get-theme-mode))
(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)))
(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 "call_function" eaf--buffer-id "get_toc")))
;; 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)
(goto-char (point-min))
(set (make-local-variable 'eaf-pdf-outline-original-buffer-name) buffer-name)
(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 "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))))
;;;;;;;;;;;;;;;;;;;; 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."
(interactive)
(let ((vars (list 'eaf-browser-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-rss-reader-keybinding
'eaf-mindmap-keybinding
'eaf-mermaid-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
* Global keybindings
| Key | Event |
|-------+-----------------------|
| C-h m | eaf-describe-bindings |
| C-c b | eaf-open-bookmark |
| C-c e | eaf-open-external |
| 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 "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 "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 "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)))
(provide 'eaf)
;;; eaf.el ends here