Skip to content
Andy Stewart edited this page Feb 28, 2022 · 1 revision

EAF Architecture

img

The easiest way to think about the design of the EAF: start a GUI application in the background, then attach the frame to the appropriate location in the Emacs window.

Some of the important architecture design ideas:

  • QGraphicsView/QGraphicsScene (Python) is used to simulate Emacs window/buffer design.
    • QGraphicsScene is similar to buffers in Emacs, it controls the state and the content details of an application.
    • QGraphicsView is similar to windows in Emacs, it populates the QGraphicsScene (buffer) to the foreground at the appropriate position.
    • The QGraphicsView instance is destroyed when the eaf-mode's window is hidden, a new QGraphicsView instance is created when the eaf-mode is shown on the foreground; while the QGraphicsScene instance stays alive in the backgrround until the user kills the eaf-mode buffer.
    • Every change in QGraphicsScene is synchronized to QGraphicsView in real-time by GPU compositing.
    • One application GraphicsScene can be used by multiple windows and hence populate at different positions on the Emacs frame.
    • By using QWindow::setParent technology, the QGraphicsView is attached to the appropriate position on the Emacs frame, so that different instances of GUI applications feels like they're part of Emacs.
  • Keyboard and Mouse Event Listeners
    • When the user types using a keyboard, it is received by the Emacs eaf-mode buffer (Elisp), Emacs then sends the key event to QGraphicsScene through EPC.
    • When the user clicks using a mouse on the window, it is received by QGraphicsView, QGraphicsView then translates and sends the mouse event coordinate to QGraphicsScene to process.
  • Multi-language collaborative programming through protocols and interfaces, to maximize the shared ecosystem.
    • Elisp <-> EPC <-> Python: use PyQt5 to utilize the rich ecosystem of Python and Qt5.
    • Elisp <-> EAF Browser <-> JavaScript: use QtWebEngine to utilize the rich ecosystem of NodeJS and JavaScript.
    • Elisp <-> EAF Browser <-> Vue.js: use QtWebEngine to utilize the rich ecosystem of Vue.js.

This design ensures that the Emacs keyboard-oriented design and its ecosystem are respected. It enables the ability to extend Emacs to numerous modern languages and ecosystems, so that the utmost extensibility which any Emacs hacker loves, is preserved.

EAF Development Handbook

Understand the EAF directory structure

Directory Explanation
app App directory, every app should locate in app/app_name/buffer.py, every app's entering file is buffer.py
core Core modules directory
core/buffer.py App's abstract interface file, includes IPC invocation, event handlers/forwarding to individual app buffer.py, dig into this if you want to modify core functionalities of EAF
core/view.py App's display interface file, includes application window cross-process displaying/positioning, you can ignore this file most of the times
core/webengine.py QtWebEngine module, core functionalities of the EAF Browser lies here, and it is the basis to extend EAF to JavaScript based applications including EAF Browser itself
core/utils.py Some common utilities that's shared by many python files to minimize duplicate code
core/pyaria2.py Aria2 module file, main purpose is to send RPC download command to the aria2 daemon to install stuff from the browser
docker Dockerfile, to construct a EAF docker image, ignore this file unless you use Docker to run EAF
screenshot App screenshots
eaf.el The main Elisp file, most core Elisp functions and definitions are defined here
eaf-org.el EAF compatibility with org-mode, optional
eaf-evil.el EAF compatibility with evil-mode, optional
eaf-all-the-icons.el EAF compatibility with all-the-icons, optional
eaf.py The main Python process file, to create the EAF process and handles Elisp requests including IPC communication interfaces
LICENSE GPLv3 License file
README.md English README
README.zh-CN.md Chinese README
setup.py Python project configuration file, only used to identify the project directory, ignore it
install-eaf.* Install scripts for different systems

Demo

There is a very simple demo application in app/demo/buffer.py that looks like the following:

from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QPushButton
from core.buffer import Buffer

class AppBuffer(Buffer):
    def __init__(self, buffer_id, url, arguments):
        Buffer.__init__(self, buffer_id, url, arguments, True)

        self.add_widget(QPushButton("Hello, EAF hacker, it's working!!!"))
        self.buffer_widget.setStyleSheet("font-size: 100px")

You can invoke the demo using M-x eaf-open-demo, it opens a Qt window with a big button, isn't it easy?

Demo

Create a new application

To create a new EAF application, you only need the following procedures:

  1. Create a new sub-directory within app, copy contents in app/demo/buffer.py into it.
  2. Create a Qt5 widget class, and use self.add_widget interface to replace the QPushButton from the demo with your new widget.
  3. Within the eaf-open Elisp function, extend it to open this new EAF app, refer how the demo application is achieved.

Understand the EAF API

Looking at the demo again,

from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QPushButton
from core.buffer import Buffer

class AppBuffer(Buffer):
    def __init__(self, buffer_id, url, arguments):
        Buffer.__init__(self, buffer_id, url, arguments, True)

        self.add_widget(QPushButton("Hello, EAF hacker, it's working!!!"))
        self.buffer_widget.setStyleSheet("font-size: 100px")

The AppBuffer is the entering class for each application, it inherits the Buffer class defined in core/buffer.py.

  • buffer_id: the unique ID sent from Emacs, used to differentiate different applications.
  • url: the application path, can be the link in the EAF Browser, or the file path of EAF PDF Viewer, the precise use depends on the application.
  • arguments: in addition to url, used to pass additional parameters from Elisp to Python, for example the temp_html_file flag in the EAF Browser, to identify whether to delete temporary files after rendering HTML

Prompt message in the Emacs minibuffer from Python

If you want prompt a message in the Emacs minibuffer, you can emit signal message_to_emacs in the AppBuffer class:

from core.utils import message_to_emacs
message_to_emacs("hello from eaf")

Set Emacs variable from Python

Use the following to set Emacs variables from Python:

from core.utils import set_emacs_var
set_emacs_var("name", "value", "eaf-specific")

Get Emacs variable from Python

Use the following to get Emacs variables from Python:

from core.utils import get_emacs_var
get_emacs_var("var-name")

Evaluate Elisp code from Python

You can evaluate any Elisp code from Python, note that you need to use Python's ''' syntax to wrap the code, in case the Elisp code contains special symbols:

from core.utils import eval_in_emacs
eval_in_emacs('''(message "hello")''')

Evaluate Python function from Elisp

It is a bit more complicated if you want to call Python function from Elisp.

def get_foo(self):
    return "Python Result"

At Elisp side, use eaf-call-sync and call_function interface to invoke the Python function.

(eaf-call-sync "call_function" eaf--buffer-id "get_foo")

eaf--buffer-id is a Elisp buffer-local variable, EAF framework will automatically identify the corresponding get_foo function in the EAF app/buffer.py and return its value.

Evaluate Python function (with parameters) from Elisp

If you want to invoke Python function with parameters, you should use call_function_with_args interface:

(eaf-call-sync "call_function_with_args" eaf--buffer-id "function_name" "function_args")

If the ELisp doesn't need the return value from the Python side, you can use eaf-call-async to replace eaf-call-sync.

Bind Python interactive function from Elisp

If you want to bind Emacs key bindings to Python functions, you need to add a decorator @interactive to the Python function, which is provided by the utils.py file; If it is a command for an app based on the WebEngine, you need to add @interactive(insert_or_do=True) decorator, so that Python side can automatically generate an insert_or_foo function for foo, so that single-keystroke keybindings can work with browser input fields.

Read user's input from Python

Below is code snippet from EAF PDF Viewer to demonstrate how to read user's input from Python:

...

class AppBuffer(Buffer):
    def __init__(self, buffer_id, url, arguments):
        Buffer.__init__(self, buffer_id, url, arguments, False)

        self.add_widget(PdfViewerWidget(url, QColor(0, 0, 0, 255)))
        self.buffer_widget.send_jump_page_message.connect(self.send_jump_page_message)

    def send_jump_page_message(self):
        self.send_input_message("Jump to Page: ", "jump_page")

    def handle_input_response(self, callback_tag, result_content):
        if callback_tag == "jump_page":
            self.buffer_widget.jump_to_page(int(result_content))

    def cancel_input_response(self, callback_tag):
        if callback_tag == "jump_page":
            ...

...
  1. First of all, in Python side, call self.send_input_message function, that takes a few parameters: a prompt, a callback tag (used to differentiate which Python function the input received from Emacs came from), an input type (choose from "string"/"file"/"yes-or-no", defaulted to be "string"), initial input contents (optional).
  2. After user enters data, the handle_input_response function can decide based on the callback tag the subsequent Python functions to run.
  3. If the user cancels input, the cancel_input_response function can decide if there's any cleanup functions to run, also based on the callback tag.

Evaluate JavaScript function from Python

If the EAF app is based on the WebEngine and JS functions or NodeJS libraries, it is suggested to learn from the app/mindmap/buffer.py code as an example, you can use the eval_js function to achieve calling JavaScript function from Python:

self.buffer_widget.eval_js("js_function(js_argument)")

The JavaScript function definitions are stored in app/foo/index.html file, unless used directly by core/js.

Evaluate JavaScript function and Return Value from Python

Similar to eval_js, except that it is changed to execute_js. The following is an example to retrieve text from the WebEngine:

text = self.buffer_widget.execute_js("get_selection();")

Rename buffer from Python

It is very easy to rename buffer from Python, simply call self.change_title("new_title").

Save/Restore session

To save session, implement the save_session_data interface, to restore session, implement the restore_session_data interface. The following is an example snippet for the EAF Video Player:

def save_session_data(self):
    return str(self.buffer_widget.media_player.position())

def restore_session_data(self, session_data):
    position = int(session_data)
    self.buffer_widget.media_player.setPosition(position)

Argument session_data is string, you can put anything in it. All session data save at ~/.emacs.d/eaf/session.json file.

Buffer Fullscreen Interface

Some applications, such as the EAF Browser needs to get into fullscreen state when double clicking a YouTube video, simply calling the following APIs can control the fullscreen state per buffer.

  • toggle_fullscreen
  • enable_fullscreen
  • disable_fullscreen

This fullscreen is buffer-only, it doesn't go into the "actual" fullscreen if there's more than one active buffer on the current window.

Send Key Event to Python from Elisp

Sometimes we want to directly send a certain key event to Python from Elisp, this is achieved by defining an Elisp function:

(defun eaf-send-return-key ()
  "Directly send return key to EAF Python side."
  (interactive)
  (eaf-call-sync "send_key" eaf--buffer-id "RET"))

Then bind it using eaf-bind-key function or modifying eaf-*-keybinding to the corresponding key. Refer to eaf.el for more examples.

Cleanup Background Processes

If the EAF App has some background process, you can use destroy_buffer interface to do some cleanup work before the EAF buffer gets closed. The following is an example comes from EAF Terminal, that kills the NodeJS background process:

def destroy_buffer(self):
    os.kill(self.background_process.pid, signal.SIGKILL)

    if self.buffer_widget is not None:
        # NOTE: We need delete QWebEnginePage manual, otherwise QtWebEngineProcess won't quit.
        self.buffer_widget.web_page.deleteLater()
        self.buffer_widget.deleteLater()

Debugging EAF

Here we share some debugging tips to speedup your development on EAF:

Python log and backtrace

If you added any print function to the Python code, or when the Python session crashed due to some error, you can switch to the *eaf* buffer from Emacs to observe the Python log.

Restart EAF, not Emacs

After starting an EAF application, EAF will automatically start a Python process in the background. If you made some changes to the Python code, you don't need to restart Emacs entirely, simply running eaf-kill-process will stop the Python process, then running again your desired EAF app will yield the new result. Or you can simply call eaf-restart-process to restart and restore every EAF application you previously opened.

Debugging with GDB

If you happen to needing gdb when debugging EAF, mostly due to segfault error, you can do the following:

  1. Run (setq eaf-enable-debug t) within Emacs to run EAF with gdb.
  2. Refer to [Restart EAF, not Emacs](#Restart EAF, not Emacs) section.
  3. Check the *eaf* buffer when encountering the error again.

Todolist

Some things you can start hacking now ;)

Clone this wiki locally