-
Notifications
You must be signed in to change notification settings - Fork 234
EAF Framework
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.
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 |
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 |
---|
To create a new EAF application, you only need the following procedures:
- Create a new sub-directory within
app
, copy contents inapp/demo/buffer.py
into it. - Create a Qt5 widget class, and use
self.add_widget
interface to replace theQPushButton
from the demo with your new widget. - Within the
eaf-open
Elisp function, extend it to open this new EAF app, refer how thedemo
application is achieved.
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 tourl
, used to pass additional parameters from Elisp to Python, for example thetemp_html_file
flag in the EAF Browser, to identify whether to delete temporary files after rendering HTML
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")
Use the following to set Emacs variables from Python:
from core.utils import set_emacs_var
set_emacs_var("name", "value", "eaf-specific")
Use the following to get Emacs variables from Python:
from core.utils import get_emacs_var
get_emacs_var("var-name")
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")''')
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.
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
.
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.
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":
...
...
- 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). - After user enters data, the
handle_input_response
function can decide based on the callback tag the subsequent Python functions to run. - 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.
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
.
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();")
It is very easy to rename buffer from Python, simply call self.change_title("new_title")
.
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.
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.
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.
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()
Here we share some debugging tips to speedup your development on EAF:
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.
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.
If you happen to needing gdb
when debugging EAF, mostly due to segfault error, you can do the following:
- Run
(setq eaf-enable-debug t)
within Emacs to run EAF withgdb
. - Refer to [Restart EAF, not Emacs](#Restart EAF, not Emacs) section.
- Check the
*eaf*
buffer when encountering the error again.