├── framework.png ├── img ├── browser.png ├── EAF_Logo.png ├── framework.png ├── EAF_Banner.png ├── file-manager.png ├── music-player.png ├── pdf-viewer.png ├── EAF_Banner_Transparent.png └── EAF_Logo_Transparent.png ├── core ├── js │ ├── get_selection_text.js │ ├── clear_focus.js │ ├── get_background_color.js │ ├── select_input_text.js │ ├── set_focus_text.js │ ├── immersive_translation_response.js │ ├── pw_autofill.js │ ├── get_next_page_url.js │ ├── get_focus_text.js │ ├── get_cursor_word.js │ ├── focus_input.js │ ├── immersive_translation.js │ └── marker.js ├── paddle_ocr.py ├── pyaria2.py ├── view.py ├── utils.py └── buffer.py ├── .gitignore ├── gnome-shell └── eaf-wayland@emacs-eaf.org │ ├── metadata.json │ └── extension.js ├── .github ├── workflows │ └── sync-eaf-resources.yaml └── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── bug_report.md │ └── config.yml ├── dependencies.json ├── extension ├── eaf-evil.el ├── eaf-mail.el ├── eaf-all-the-icons.el ├── eaf-org.el └── eaf-interleave.el ├── reinput └── main.c ├── CODE_OF_CONDUCT.md ├── sync-eaf-resources.py ├── applications.json ├── README.zh-CN.md ├── README.md └── install-eaf.py /framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-eaf/emacs-application-framework/HEAD/framework.png -------------------------------------------------------------------------------- /img/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-eaf/emacs-application-framework/HEAD/img/browser.png -------------------------------------------------------------------------------- /img/EAF_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-eaf/emacs-application-framework/HEAD/img/EAF_Logo.png -------------------------------------------------------------------------------- /img/framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-eaf/emacs-application-framework/HEAD/img/framework.png -------------------------------------------------------------------------------- /img/EAF_Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-eaf/emacs-application-framework/HEAD/img/EAF_Banner.png -------------------------------------------------------------------------------- /img/file-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-eaf/emacs-application-framework/HEAD/img/file-manager.png -------------------------------------------------------------------------------- /img/music-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-eaf/emacs-application-framework/HEAD/img/music-player.png -------------------------------------------------------------------------------- /img/pdf-viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-eaf/emacs-application-framework/HEAD/img/pdf-viewer.png -------------------------------------------------------------------------------- /core/js/get_selection_text.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | return window.getSelection().toString().substr(0, 20); 3 | })(); 4 | -------------------------------------------------------------------------------- /img/EAF_Banner_Transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-eaf/emacs-application-framework/HEAD/img/EAF_Banner_Transparent.png -------------------------------------------------------------------------------- /img/EAF_Logo_Transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-eaf/emacs-application-framework/HEAD/img/EAF_Logo_Transparent.png -------------------------------------------------------------------------------- /core/js/clear_focus.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const activeElement = document.activeElement; 3 | activeElement.blur(); 4 | })(); 5 | -------------------------------------------------------------------------------- /core/js/get_background_color.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | return window.getComputedStyle(document.body, null).backgroundColor.toString(); 3 | })(); 4 | -------------------------------------------------------------------------------- /core/js/select_input_text.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const activeElement = document.activeElement; 3 | activeElement.focus(); 4 | activeElement.select(); 5 | })(); 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | *.pyc 3 | /.log/ 4 | app/ 5 | app/* 6 | node_modules/ 7 | dist/ 8 | tags 9 | /.eaf-installed-apps.json 10 | compile_commands.json 11 | /reinput/reinput 12 | .cache/ 13 | .aider* 14 | -------------------------------------------------------------------------------- /core/paddle_ocr.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if __name__ == "__main__": 4 | image_path = sys.argv[1] 5 | from paddleocr import PaddleOCR 6 | ocr = PaddleOCR() 7 | result = ocr.ocr(image_path) 8 | string = ''.join(list(map(lambda r: r[1][0], result[0]))).replace(" ,", ",") 9 | print(string, flush=True) 10 | -------------------------------------------------------------------------------- /gnome-shell/eaf-wayland@emacs-eaf.org/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Expose a D-Bus interface to help EAF running on Wayland", 3 | "name": "EAF Waynland Extension", 4 | "shell-version": [ 5 | "43.1" 6 | ], 7 | "url": "https://github.com/emacs-eaf/emacs-application-framework", 8 | "uuid": "eaf-wayland@emacs-eaf.org", 9 | "version": 1 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/sync-eaf-resources.yaml: -------------------------------------------------------------------------------- 1 | name: Sync EAF repos with mirror 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | schedule: 7 | - cron: '*/30 * * * *' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 30 13 | steps: 14 | - name: 'Checkout' 15 | uses: actions/checkout@v2 16 | 17 | - name: 'Set up Python' 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: 3.12 21 | 22 | - name: 'Syncing' 23 | env: 24 | EAF_MIRROR_USERNAME: ${{ secrets.EAF_MIRROR_USERNAME }} 25 | EAF_MIRROR_PASSWORD: ${{ secrets.EAF_MIRROR_PASSWORD }} 26 | run: python sync-eaf-resources.py --really-run 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for the Emacs Application Framework 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of an alternative solutions or features you've considered, if any. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Ensure you're on the latest master branch, then note the steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Versions (please complete the following info):** 20 | - Distro and DE/WM: 21 | - Versions of Dependencies: 22 | - M-x emacs-version: 23 | 24 | **Error logs** 25 | Please check the `*eaf*` buffer, if there is any error in it, paste it here. 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /core/js/set_focus_text.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | let newText = "%{new_text_base64}"; 3 | const activeElement = document.activeElement; 4 | 5 | // Decode the base64 encoded text 6 | const decodedText = decodeURIComponent(escape(window.atob(newText))); 7 | 8 | // Check if the active element is a typical input or textarea 9 | if ("value" in activeElement) { 10 | activeElement.value = decodedText; 11 | } else { 12 | // For contenteditable elements or others (like divs for Telegram and Google Gemini), 13 | // directly set textContent. This approach works uniformly across various types of elements. 14 | activeElement.textContent = decodedText; 15 | } 16 | 17 | // Simulate input event on the active element or a found contenteditable element 18 | // after setting the text. Some websites need input event before form submission. 19 | var event = new Event('input', { bubbles: true, cancelable: true }); 20 | activeElement.dispatchEvent(event); 21 | })(); 22 | -------------------------------------------------------------------------------- /core/js/immersive_translation_response.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | let translates = %1; 3 | 4 | const elements = document.getElementsByClassName("eaf-translated"); 5 | 6 | for (let i = 0; i < elements.length; i++) { 7 | const classNames = elements[i].classList; 8 | 9 | const targetClassName = Array.from(classNames).find((className) => 10 | className.startsWith("eaf-translated-node-") 11 | ); 12 | 13 | if (targetClassName) { 14 | const index = parseInt(targetClassName.split("-")[3]); 15 | 16 | let translatedNode = elements[i]; 17 | 18 | translatedNode.style.display = 'block'; 19 | translatedNode.style.whiteSpace = 'pre-wrap'; 20 | translatedNode.style.borderLeft = '4px solid #C53D56 !important'; 21 | translatedNode.style.paddingLeft = '12px !important'; 22 | translatedNode.style.marginTop = '4px'; 23 | translatedNode.style.marginBottom = '4px'; 24 | translatedNode.style.paddingTop = '4px'; 25 | translatedNode.style.paddingBottom = '4px'; 26 | 27 | translatedNode.innerHTML = translates[index]; 28 | } 29 | } 30 | 31 | console.log("***** ", translates); 32 | })(); 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: "READ FIRST ☛ Mandatory Procedures to Keep Your EAF Up-To-Date" 4 | url: https://github.com/emacs-eaf/emacs-application-framework/discussions/527?sort=new 5 | about: Read first if you're not a new user, your problem might be addressed already. 6 | - name: "APP SUPPORT ☛ App Repository" 7 | url: https://github.com/orgs/emacs-eaf/repositories 8 | about: Please search, ask and answer EAF Application specific issue in its own repository. 9 | - name: "SUPPORT ☛ The Discussions feature of this repository" 10 | url: https://github.com/emacs-eaf/emacs-application-framework/discussions 11 | about: Please search, ask and answer questions here. 12 | - name: "SUPPORT ☛ Emacs China" 13 | url: https://emacs-china.org/ 14 | about: Another place to search, ask and answer questions. 15 | - name: "SUPPORT ☛ Emacs Reddit" 16 | url: https://www.reddit.com/r/emacs 17 | about: Another place to search, ask and answer questions. 18 | - name: "EAF FAQ" 19 | url: https://github.com/emacs-eaf/emacs-application-framework/wiki/FAQ 20 | about: It might be that many others had the same question. 21 | - name: "EAF Wiki" 22 | url: https://github.com/emacs-eaf/emacs-application-framework/wiki 23 | about: The fine wiki may also be of use. 24 | -------------------------------------------------------------------------------- /core/js/pw_autofill.js: -------------------------------------------------------------------------------- 1 | function retrievePasswordFromPage() { 2 | let password = ""; 3 | let formData = {}; 4 | let inputList = document.getElementsByTagName("input"); 5 | for(let i=0;i= document.body.offsetHeight - 1) { 5 | if (pageUrl.startsWith("https://www.google.com")) { 6 | var pageIndexes = Array.from(document.getElementsByTagName("td")); 7 | var pages = pageIndexes.filter(index => index.children.length > 0); 8 | var pageTypes = pages.map(page => page.children[0].tagName); 9 | 10 | var currentPageIndex = pageTypes.lastIndexOf("SPAN"); 11 | 12 | if (currentPageIndex < pages.length - 2) { 13 | return pages[currentPageIndex + 1].children[0].href; 14 | } 15 | } else if (pageUrl.startsWith("https://www.bing.com")) { 16 | var pageIndexes = Array.from(document.querySelectorAll(".sb_bp")); 17 | var pageUrls = pageIndexes.map(page => page.href); 18 | 19 | var currentPageIndex = pageUrls.lastIndexOf(""); 20 | 21 | if (currentPageIndex < pageUrls.length - 2) { 22 | return pageUrls[currentPageIndex + 1]; 23 | } 24 | } else if (pageUrl.startsWith("https://www.baidu.com")) { 25 | var pageIndexes = Array.from(document.querySelectorAll("#page")[0].children[0].children); 26 | var pageUrls = pageIndexes.map(page => page.tagName); 27 | 28 | var currentPageIndex = pageUrls.lastIndexOf("STRONG"); 29 | 30 | if (currentPageIndex < pageUrls.length - 2) { 31 | return pageIndexes[currentPageIndex + 1].href; 32 | } 33 | } 34 | } 35 | 36 | return ""; 37 | })(); 38 | -------------------------------------------------------------------------------- /core/js/get_focus_text.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const activeElement = document.activeElement; 3 | var inputs = ["input", "select", "textarea"]; 4 | var inputTypes = ["text", "textarea", "email", "password", "tel", "search"]; 5 | var pageUrl = window.location.href; 6 | var tagName = activeElement.tagName.toLowerCase(); 7 | 8 | if (pageUrl === "https://mail.qq.com/" && activeElement) { 9 | // QQ mail have some security mechanism that we can't fetch value of activeElement. 10 | // So we just return empty string make is_focus method works well in browser.py 11 | return ""; 12 | } else if (pageUrl === "https://mail.163.com/" && activeElement) { 13 | // QQ mail have some security mechanism that we can't fetch value of activeElement. 14 | // So we just return empty string make is_focus method works well in browser.py 15 | return ""; 16 | } else if (activeElement && 17 | inputs.indexOf(tagName) !== -1 && 18 | inputTypes.indexOf(activeElement.type) !== -1 19 | ) { 20 | return activeElement.value; 21 | } else if (activeElement.isContentEditable) { 22 | // For the Rich Text Editor 23 | return activeElement.textContent; 24 | } else { 25 | if (pageUrl.startsWith("https://web.telegram.org/") && activeElement.hasAttribute("placeholder")) { 26 | return activeElement.textContent; 27 | } else if ((pageUrl.startsWith("https://console.cloud.google.com/") || pageUrl.startsWith("https://ssh.cloud.google.com/")) && tagName == "iframe") { 28 | // Make user can type text in Terminal of Google Cloud. 29 | return ""; 30 | } else { 31 | return undefined; 32 | } 33 | } 34 | })(); 35 | -------------------------------------------------------------------------------- /gnome-shell/eaf-wayland@emacs-eaf.org/extension.js: -------------------------------------------------------------------------------- 1 | import Gio from 'gi://Gio'; 2 | 3 | const EAFWaylandInterface = ` 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | `; 15 | 16 | export default class wayland { 17 | 18 | dbus; 19 | 20 | enable() { 21 | this.dbus = Gio.DBusExportedObject.wrapJSObject( 22 | EAFWaylandInterface, 23 | this, 24 | ); 25 | this.dbus.export( 26 | Gio.DBus.session, 27 | '/org/eaf/wayland', 28 | ); 29 | } 30 | 31 | disable() { 32 | this.dbus.unexport_from_connection( 33 | Gio.DBus.session, 34 | ); 35 | this.dbus = undefined; 36 | } 37 | 38 | get_windows() { 39 | return global.get_window_actors() 40 | .map(w => ({id: w.toString(), 41 | ref: w, 42 | title: w.get_meta_window().get_wm_class()})) 43 | .filter(w => !w.title.includes('Gnome-shell')); 44 | } 45 | 46 | get_active_window() { 47 | return this.get_windows().slice(-1)[0].title 48 | } 49 | 50 | get_emacs_window_coordinate() { 51 | const match_windows = global.get_window_actors().filter(w => w.get_meta_window().get_wm_class() == "emacs") 52 | if (match_windows[0] == undefined) { 53 | return "0,0" 54 | } else { 55 | const emacs_window = match_windows[0] 56 | const rect = emacs_window.get_meta_window().get_frame_rect() 57 | return rect.x + "," + rect.y 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/pyaria2.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python 3 | # coding=utf-8 4 | 5 | class Jsonrpc(object): 6 | 7 | MUTI_METHOD = 'system.multicall' 8 | ADDURI_METHOD = 'aria2.addUri' 9 | 10 | def __init__(self, host, port, token=None): 11 | self._idCount = 0 12 | self.host = host 13 | self.port = port 14 | self.serverUrl = "http://{host}:{port}/jsonrpc".format(**locals()) 15 | 16 | def _genParams(self, method , uris=None, options=None, cid=None): 17 | p = { 18 | 'jsonrpc': '2.0', 19 | 'id': self._idCount, 20 | 'method': method, 21 | 'test': 'test', 22 | 'params': [] 23 | } 24 | if uris: 25 | p['params'].append(uris) 26 | if options: 27 | p['params'].append(options) 28 | return p 29 | 30 | def _post(self, action, params, onSuccess, onFail=None): 31 | import requests 32 | import json 33 | 34 | if onFail is None: 35 | onFail = Jsonrpc._defaultErrorHandle 36 | paramsObject = self._genParams(action, *params) 37 | resp = requests.post(self.serverUrl, data=json.dumps(paramsObject)) 38 | result = resp.json() 39 | if "error" in result: 40 | return onFail(result["error"]["code"], result["error"]["message"]) 41 | else: 42 | return onSuccess(resp) 43 | 44 | def addUris(self, uri, options=None): 45 | def success(response): 46 | return response.text 47 | return self._post(Jsonrpc.ADDURI_METHOD, [[uri,], options], success) 48 | 49 | 50 | @staticmethod 51 | def _defaultErrorHandle(code, message): 52 | print ("ERROR: {},{}".format(code, message)) 53 | return None 54 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "pacman": [ 3 | "wmctrl", 4 | "nodejs", 5 | "npm", 6 | "python-pyqt6-webengine", 7 | "python-pyqt6", 8 | "python-pyqt6-sip", 9 | "qt6-multimedia", 10 | "qt6-svg", 11 | "libvdpau-va-gl" 12 | ], 13 | "guix": [ 14 | "wmctrl", 15 | "node" 16 | ], 17 | "nix": [ 18 | "nixpkgs#wmctrl", 19 | "nixpkgs#nodejs" 20 | ], 21 | "emerge": [ 22 | "x11-misc/wmctrl", 23 | "dev-python/gssapi", 24 | "net-libs/nodejs" 25 | ], 26 | "apt": [ 27 | "wmctrl", 28 | "nodejs", 29 | "npm", 30 | "libxcb-cursor-dev" 31 | ], 32 | "dnf": [ 33 | "wmctrl", 34 | "nodejs", 35 | "npm" 36 | ], 37 | "pkg": [ 38 | "wmctrl", 39 | "node", 40 | "npm" 41 | ], 42 | "zypper": [ 43 | "wmctrl", 44 | "nodejs16", 45 | "npm16" 46 | ], 47 | "brew": [ 48 | "node" 49 | ], 50 | "pip": { 51 | "pacman": [ 52 | "epc", 53 | "sexpdata==1.0.0", 54 | "tld", 55 | "lxml" 56 | ], 57 | "linux": [ 58 | "epc", 59 | "sexpdata==1.0.0", 60 | "tld", 61 | "lxml", 62 | "PyQt6==6.5.0", 63 | "PyQt6-Qt6==6.5.0", 64 | "PyQt6-sip", 65 | "PyQt6-WebEngine==6.5.0", 66 | "PyQt6-WebEngine-Qt6==6.5.0" 67 | ], 68 | "win32": [ 69 | "epc", 70 | "sexpdata==1.0.0", 71 | "tld", 72 | "lxml", 73 | "pygetwindow", 74 | "PyQt6==6.5.0", 75 | "PyQt6-Qt6==6.5.0", 76 | "PyQt6-sip", 77 | "PyQt6-WebEngine==6.5.0", 78 | "PyQt6-WebEngine-Qt6==6.5.0" 79 | ], 80 | "darwin": [ 81 | "epc", 82 | "sexpdata==1.0.0", 83 | "tld", 84 | "lxml", 85 | "mac-app-frontmost", 86 | "PyQt6==6.5.0", 87 | "PyQt6-Qt6==6.5.0", 88 | "PyQt6-sip", 89 | "PyQt6-WebEngine==6.5.0", 90 | "PyQt6-WebEngine-Qt6==6.5.0" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /core/js/get_cursor_word.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const getWordAtPoint = (x, y) => { 3 | const element = document.elementFromPoint(x, y); 4 | 5 | if (element && (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA')) { 6 | // Simulate a click at the current cursor position 7 | const clickEvent = new MouseEvent('click', { 8 | view: window, 9 | bubbles: true, 10 | cancelable: true, 11 | clientX: x, 12 | clientY: y, 13 | }); 14 | document.body.dispatchEvent(clickEvent); 15 | 16 | // Then focus on the form element 17 | const inputElement = element; 18 | inputElement.focus(); 19 | 20 | // Get the word at the cursor position 21 | const cursorPosition = inputElement.selectionStart; 22 | const inputValue = inputElement.value; 23 | 24 | let start = cursorPosition; 25 | while (start > 0 && !/\s/.test(inputValue[start - 1])) { 26 | start--; 27 | } 28 | 29 | let end = cursorPosition; 30 | while (end < inputValue.length && !/\s/.test(inputValue[end])) { 31 | end++; 32 | } 33 | 34 | return inputValue.substring(start, end); 35 | } else { 36 | const range = document.caretRangeFromPoint(x, y); 37 | if (range && range.startContainer.nodeType === Node.TEXT_NODE) { 38 | const data = range.startContainer.data; 39 | const offset = range.startOffset; 40 | 41 | let start = offset; 42 | while (start > 0 && !/\s/.test(data[start - 1])) { 43 | start--; 44 | } 45 | 46 | let end = offset; 47 | while (end < data.length && !/\s/.test(data[end])) { 48 | end++; 49 | } 50 | 51 | return data.substring(start, end); 52 | } 53 | } 54 | return null; 55 | }; 56 | 57 | var mouseX = parseInt("%{mouse_x}"); 58 | var mouseY = parseInt("%{mouse_y}"); 59 | const word = getWordAtPoint(mouseX, mouseY); 60 | return word; 61 | })(); 62 | -------------------------------------------------------------------------------- /core/js/focus_input.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function getVisibleElements(filter) { 3 | var all = Array.from(document.documentElement.getElementsByTagName("*")); 4 | var visibleElements = []; 5 | for (var i = 0; i < all.length; i++) { 6 | var e = all[i]; 7 | // Include elements in a shadowRoot. 8 | if (e.shadowRoot) { 9 | var cc = e.shadowRoot.querySelectorAll('*'); 10 | for (var j = 0; j < cc.length; j++) { 11 | all.push(cc[j]); 12 | } 13 | } 14 | var rect = e.getBoundingClientRect(); 15 | if ((rect.top <= window.innerHeight) && (rect.bottom >= 0) 16 | && (rect.left <= window.innerWidth) && (rect.right >= 0) 17 | && rect.height > 0 18 | && getComputedStyle(e).visibility !== 'hidden' 19 | ) { 20 | filter(e, visibleElements); 21 | } 22 | } 23 | return visibleElements; 24 | } 25 | var cssSelector = "input, textarea, [contenteditable='true']"; 26 | 27 | var elements = getVisibleElements(function(e, v) { 28 | if ((e.matches('input') && !e.disabled && !e.readOnly && 29 | (e.type === "text" || e.type === "search" || e.type === "password")) || 30 | (e.matches('textarea') && !e.disabled && !e.readOnly) || 31 | (e.contentEditable === "true")) { 32 | v.push(e); 33 | } 34 | }); 35 | 36 | if (elements.length === 0 && document.querySelector(cssSelector) !== null) { 37 | document.querySelector(cssSelector).scrollIntoView(); 38 | elements = getVisibleElements(function(e, v) { 39 | if ((e.matches(cssSelector) && !e.disabled && !e.readOnly) || 40 | (e.contentEditable === "true")) { 41 | v.push(e); 42 | } 43 | }); 44 | } 45 | 46 | if (elements.length >= 1) { 47 | var focusElement = elements[0]; 48 | if (focusElement.contentEditable === "true") { 49 | // For contenteditable elements, setting focus is slightly different 50 | var range = document.createRange(); 51 | var sel = window.getSelection(); 52 | range.selectNodeContents(focusElement); 53 | sel.removeAllRanges(); 54 | sel.addRange(range); 55 | } else { 56 | // For input and textarea elements 57 | var value = focusElement.value; 58 | focusElement.focus(); 59 | focusElement.value = ""; 60 | focusElement.value = value; 61 | } 62 | } 63 | })(); 64 | -------------------------------------------------------------------------------- /extension/eaf-evil.el: -------------------------------------------------------------------------------- 1 | ;;; eaf-evil.el --- Emacs application framework -*- lexical-binding: t; -*- 2 | 3 | ;; Filename: eaf-evil.el 4 | ;; Description: Emacs application framework 5 | ;; Author: lee 6 | ;; Maintainer: Andy Stewart 7 | ;; Copyright (C) 2018, Andy Stewart, all rights reserved. 8 | ;; Created: 2020-05-17 12:31:12 9 | ;; Version: 0.5 10 | ;; Last-Updated: Wed Aug 11 17:04:08 2021 (-0400) 11 | ;; By: Mingde (Matthew) Zeng 12 | ;; URL: https://github.com/emacs-eaf/emacs-application-framework 13 | ;; Keywords: 14 | ;; Compatibility: emacs-version >= 27 15 | ;; 16 | ;; Features that might be required by this library: 17 | ;; 18 | ;; Please check README 19 | ;; 20 | 21 | ;;; This file is NOT part of GNU Emacs 22 | 23 | ;;; License 24 | ;; 25 | ;; This program is free software; you can redistribute it and/or modify 26 | ;; it under the terms of the GNU General Public License as published by 27 | ;; the Free Software Foundation; either version 3, or (at your option) 28 | ;; any later version. 29 | 30 | ;; This program is distributed in the hope that it will be useful, 31 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | ;; GNU General Public License for more details. 34 | 35 | ;; You should have received a copy of the GNU General Public License 36 | ;; along with this program; see the file COPYING. If not, write to 37 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 38 | ;; Floor, Boston, MA 02110-1301, USA. 39 | 40 | (defcustom eaf-evil-leader-key "C-SPC" 41 | "Leader key trigger" ) 42 | 43 | (defcustom eaf-evil-leader-keymap #'doom/leader 44 | "Leader key bind" 45 | :type 'keymap) 46 | 47 | ;;;###autoload 48 | (defun eaf-enable-evil-intergration () 49 | "EAF evil intergration." 50 | (interactive) 51 | 52 | (add-hook 'evil-normal-state-entry-hook 53 | (lambda () 54 | (when (derived-mode-p 'eaf-mode) 55 | (define-key eaf-mode-map (kbd eaf-evil-leader-key) eaf-evil-leader-keymap) 56 | (setq emulation-mode-map-alists 57 | (delq 'evil-mode-map-alist emulation-mode-map-alists))))) 58 | 59 | (add-to-list 'evil-insert-state-modes 'eaf-edit-mode) 60 | 61 | (eaf-bind-key clear_focus "" eaf-browser-keybinding)) 62 | 63 | (with-eval-after-load "eaf" 64 | (eaf-enable-evil-intergration)) 65 | 66 | (provide 'eaf-evil) 67 | 68 | ; don't use-package byte-compile to suppress clear_focus error 69 | ; https://github.com/melpa/melpa/issues/1817#issuecomment-47467282 70 | ;; Local Variables: 71 | ;; no-byte-compile: t 72 | ;; End: 73 | -------------------------------------------------------------------------------- /reinput/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #define MAX_KEY_CODE 248 21 | 22 | char command[50]; 23 | unsigned short focus_on_eaf; 24 | 25 | static int open_restricted(const char *path, int flags, void *user_data) 26 | { 27 | bool *grab = user_data; 28 | int fd = open(path, flags); 29 | 30 | grab && *grab && ioctl(fd, EVIOCGRAB, (void*) 1); 31 | 32 | return fd < 0 ? -errno : fd; 33 | } 34 | 35 | static void close_restricted(int fd, void *user_data) 36 | { 37 | close(fd); 38 | } 39 | 40 | struct libinput *li; 41 | struct libinput_event *ev; 42 | struct libinput_event_keyboard *kev; 43 | const static struct libinput_interface interface = { 44 | .open_restricted = open_restricted, 45 | .close_restricted = close_restricted, 46 | }; 47 | char kbd_devnode[5][19] = {0}; 48 | int8_t kbd_devnode_n = 0; 49 | 50 | static void get_kbd_device(void) 51 | { 52 | struct udev *udev = udev_new(); 53 | struct udev_device *udev_device; 54 | struct libinput_device *dev; 55 | const char *devnode; 56 | 57 | li = libinput_udev_create_context(&interface, NULL, udev); 58 | libinput_udev_assign_seat(li, "seat0"); 59 | 60 | libinput_dispatch(li); 61 | while ((ev = libinput_get_event(li))) { 62 | dev = libinput_event_get_device(ev); 63 | 64 | if (libinput_device_has_capability(dev, LIBINPUT_DEVICE_CAP_KEYBOARD) 65 | && libinput_device_keyboard_has_key(dev, KEY_A) 66 | && libinput_event_get_type(ev) == LIBINPUT_EVENT_DEVICE_ADDED 67 | && kbd_devnode_n < 5) { 68 | udev_device = libinput_device_get_udev_device(dev); 69 | devnode = udev_device_get_devnode(udev_device); 70 | 71 | strcpy(kbd_devnode[kbd_devnode_n], devnode); 72 | kbd_devnode_n++; 73 | 74 | udev_device_unref(udev_device); 75 | } 76 | 77 | libinput_device_unref(dev); 78 | libinput_event_destroy(ev); 79 | libinput_dispatch(li); 80 | } 81 | 82 | udev_unref(udev); 83 | libinput_unref(li); 84 | } 85 | 86 | static void init_libinput(void) 87 | { 88 | bool grab = 1; 89 | 90 | get_kbd_device(); 91 | 92 | li = libinput_path_create_context(&interface, &grab); 93 | while (kbd_devnode_n--) { 94 | libinput_path_add_device(li, kbd_devnode[kbd_devnode_n]); 95 | } 96 | } 97 | 98 | struct libevdev *dev; 99 | struct libevdev_uinput *uidev; 100 | 101 | static void init_libevdev(void) 102 | { 103 | dev = libevdev_new(); 104 | libevdev_set_name(dev, "reinput"); 105 | libevdev_enable_event_type(dev, EV_KEY); 106 | 107 | for (uint8_t code = 0; code <= MAX_KEY_CODE; code++) { 108 | libevdev_enable_event_code(dev, EV_KEY, code, NULL); 109 | } 110 | 111 | libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, &uidev); 112 | } 113 | 114 | static void focus_emacs(void) 115 | { 116 | system(command); 117 | } 118 | 119 | static void reinput_key(void) 120 | { 121 | if (focus_on_eaf) { 122 | focus_emacs(); 123 | focus_on_eaf = 0; 124 | } 125 | 126 | uint32_t code = libinput_event_keyboard_get_key(kev); 127 | enum libinput_key_state state = libinput_event_keyboard_get_key_state(kev); 128 | 129 | if (state == LIBINPUT_KEY_STATE_PRESSED) { 130 | libevdev_uinput_write_event(uidev, EV_KEY, code, 1); 131 | } else { 132 | libevdev_uinput_write_event(uidev, EV_KEY, code, 0); 133 | } 134 | 135 | libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); 136 | } 137 | 138 | static void handle_input(void) 139 | { 140 | libinput_dispatch(li); 141 | while ((ev = libinput_get_event(li))) { 142 | if (libinput_event_get_type(ev) == LIBINPUT_EVENT_KEYBOARD_KEY) { 143 | kev = libinput_event_get_keyboard_event(ev); 144 | reinput_key(); 145 | } 146 | 147 | libinput_event_destroy(ev); 148 | libinput_dispatch(li); 149 | } 150 | } 151 | 152 | static void *listen(void *_) 153 | { 154 | struct pollfd fds; 155 | 156 | fds.fd = libinput_get_fd(li); 157 | fds.events = POLLIN; 158 | fds.revents = 0; 159 | 160 | do { 161 | handle_input(); 162 | } while (poll(&fds, 1, -1) > -1); 163 | 164 | return NULL; 165 | } 166 | 167 | static void init_focus_command(pid_t pid) 168 | { 169 | char desktop[20]; 170 | 171 | if (getenv("XDG_CURRENT_DESKTOP")) { 172 | strcpy(desktop, getenv("XDG_CURRENT_DESKTOP")); 173 | } else { 174 | strcpy(desktop, getenv("XDG_SESSION_DESKTOP")); 175 | } 176 | 177 | if (!strcmp(desktop, "sway")) { 178 | sprintf(command, "swaymsg '[pid=%d] focus'", pid); 179 | } else if (!strcmp(desktop, "Hyprland")) { 180 | sprintf(command, "hyprctl dispatch focuswindow pid:%d", pid); 181 | } 182 | } 183 | 184 | int main(int argc, const char **argv) 185 | { 186 | pid_t pid; 187 | pthread_t thread; 188 | 189 | pid = atoi(argv[1]); 190 | 191 | init_focus_command(pid); 192 | init_libinput(); 193 | init_libevdev(); 194 | 195 | pthread_create(&thread, NULL, listen, NULL); 196 | 197 | focus_on_eaf = 0; 198 | while (1) { 199 | scanf("%hu", &focus_on_eaf); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /extension/eaf-mail.el: -------------------------------------------------------------------------------- 1 | ;;; eaf-mail.el --- Mail plugins 2 | 3 | ;; Filename: eaf-mail.el 4 | ;; Description: Mail plugins 5 | ;; Author: Andy Stewart 6 | ;; Maintainer: Andy Stewart 7 | ;; Copyright (C) 2021, Andy Stewart, all rights reserved. 8 | ;; Created: 2021-07-20 22:27:26 9 | ;; Version: 0.1 10 | ;; Last-Updated: 2021-07-20 22:27:26 11 | ;; By: Andy Stewart 12 | ;; URL: http://www.emacswiki.org/emacs/download/eaf-mail.el 13 | ;; Keywords: 14 | ;; Compatibility: GNU Emacs 28.0.50 15 | ;; 16 | ;; Features that might be required by this library: 17 | ;; 18 | ;; 19 | ;; 20 | 21 | ;;; This file is NOT part of GNU Emacs 22 | 23 | ;;; License 24 | ;; 25 | ;; This program is free software; you can redistribute it and/or modify 26 | ;; it under the terms of the GNU General Public License as published by 27 | ;; the Free Software Foundation; either version 3, or (at your option) 28 | ;; any later version. 29 | 30 | ;; This program is distributed in the hope that it will be useful, 31 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | ;; GNU General Public License for more details. 34 | 35 | ;; You should have received a copy of the GNU General Public License 36 | ;; along with this program; see the file COPYING. If not, write to 37 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 38 | ;; Floor, Boston, MA 02110-1301, USA. 39 | 40 | ;;; Commentary: 41 | ;; 42 | ;; Mail plugins 43 | ;; 44 | 45 | ;;; Installation: 46 | ;; 47 | ;; Put eaf-mail.el to your load-path. 48 | ;; The load-path is usually ~/elisp/. 49 | ;; It's set in your ~/.emacs like this: 50 | ;; (add-to-list 'load-path (expand-file-name "~/elisp")) 51 | ;; 52 | ;; And the following to your ~/.emacs startup file. 53 | ;; 54 | ;; (require 'eaf-mail) 55 | ;; 56 | ;; No need more. 57 | 58 | ;;; Customize: 59 | ;; 60 | ;; 61 | ;; 62 | ;; All of the above can customize by: 63 | ;; M-x customize-group RET eaf-mail RET 64 | ;; 65 | 66 | ;;; Change log: 67 | ;; 68 | ;; 2021/07/20 69 | ;; * First released. 70 | ;; 71 | 72 | ;;; Acknowledgements: 73 | ;; 74 | ;; 75 | ;; 76 | 77 | ;;; TODO 78 | ;; 79 | ;; 80 | ;; 81 | 82 | ;;; Require 83 | 84 | 85 | ;;; Code: 86 | 87 | (defcustom eaf-mua-get-html 88 | '(("^gnus-" . eaf-gnus-get-html) 89 | ("^mu4e-" . eaf-mu4e-get-html) 90 | ("^notmuch-" . eaf-notmuch-get-html)) 91 | "An alist regex mapping a MUA `major-mode' to a function to retrieve HTML part of a mail." 92 | :type 'alist) 93 | 94 | (defun eaf--gnus-htmlp (part) 95 | "Determine whether the gnus mail PART is HTML." 96 | (when-let ((type (mm-handle-type part))) 97 | (string= "text/html" (car type)))) 98 | 99 | (defun eaf--notmuch-htmlp (part) 100 | "Determine whether the notmuch mail PART is HTML." 101 | (when-let ((type (plist-get part :content-type))) 102 | (string= "text/html" type))) 103 | 104 | (defun eaf--get-html-func () 105 | "The function returning a function used to extract HTML of different MUAs." 106 | (catch 'get-html 107 | (cl-loop for (regex . func) in eaf-mua-get-html 108 | do (when (string-match regex (symbol-name major-mode)) 109 | (throw 'get-html func)) 110 | finally return (error "[EAF] You are either not in a MUA buffer or your MUA is not supported!")))) 111 | 112 | (defun eaf-gnus-get-html () 113 | "Retrieve HTML part of a gnus mail." 114 | (with-current-buffer gnus-original-article-buffer 115 | (when-let* ((dissect (mm-dissect-buffer t t)) 116 | (buffer (if (bufferp (car dissect)) 117 | (when (eaf--gnus-htmlp dissect) 118 | (car dissect)) 119 | (car (cl-find-if #'eaf--gnus-htmlp (cdr dissect)))))) 120 | (with-current-buffer buffer 121 | (buffer-string))))) 122 | 123 | (defun eaf-mu4e-get-html () 124 | "Retrieve HTML part of a mu4e mail." 125 | (let ((msg (or (bound-and-true-p mu4e~view-message) mu4e--view-message))) 126 | (mu4e-message-field msg :body-html))) 127 | 128 | (defun eaf-notmuch-get-html () 129 | "Retrieve HTML part of a notmuch mail." 130 | (when-let* ((msg (cond ((derived-mode-p 'notmuch-show-mode) 131 | (notmuch-show-get-message-properties)) 132 | ((derived-mode-p 'notmuch-tree-mode) 133 | (notmuch-tree-get-message-properties)) 134 | (t nil))) 135 | (body (plist-get msg :body)) 136 | (parts (car body)) 137 | (content (plist-get parts :content)) 138 | (part (if (listp content) 139 | (cl-find-if #'eaf--notmuch-htmlp content) 140 | (when (eaf--notmuch-htmlp parts) 141 | parts)))) 142 | (notmuch-get-bodypart-text msg part notmuch-show-process-crypto))) 143 | 144 | ;;;###autoload 145 | (defun eaf-open-mail-as-html () 146 | "Open the html mail in EAF Browser. 147 | 148 | The value of `mail-user-agent' must be a KEY of the alist `eaf-mua-get-html'. 149 | 150 | In that way the corresponding function will be called to retrieve the HTML 151 | part of the current mail." 152 | (interactive) 153 | (when-let* ((html (funcall (eaf--get-html-func))) 154 | (default-directory (eaf--non-remote-default-directory)) 155 | (file (concat (temporary-file-directory) (make-temp-name "eaf-mail-") ".html"))) 156 | (with-temp-file file 157 | (insert html)) 158 | (eaf-open file "browser" "temp_html_file"))) 159 | 160 | (provide 'eaf-mail) 161 | 162 | ;;; eaf-mail.el ends here 163 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | matthewzmd@posteo.net. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /core/js/immersive_translation.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function getTextNodes(node, nodes = []) { 3 | if (isHeaderOrFooter(node) || isHideNode(node)) { 4 | } else if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) { 5 | nodes.push(node); 6 | } else if (node.tagName === 'P' || 7 | node.tagName == "PRE" || 8 | node.tagName == "A") { 9 | nodes.push(node); 10 | } else { 11 | for (const child of node.childNodes) { 12 | getTextNodes(child, nodes); 13 | } 14 | } 15 | return nodes; 16 | } 17 | 18 | function isHeaderOrFooter(node) { 19 | if (!node || !node.classList) { 20 | return false; 21 | } 22 | 23 | return node.tagName === "HEADER" || node.tagName === "FOOTER" || node.classList.contains('header') || node.classList.contains('footer'); 24 | } 25 | 26 | function isHideNode(node) { 27 | if (!node || !node.classList) { 28 | return false; 29 | } 30 | 31 | var pageUrl = window.location.href; 32 | if (pageUrl.startsWith("https://www.reddit.com")) { 33 | if ((node.hasAttribute("data-adclicklocation") && node.getAttribute("data-adclicklocation") === "top_bar") || 34 | (node.hasAttribute("data-testid") && node.getAttribute("data-testid") === "post-comment-header") || 35 | node.id === 'CommentSort--SortPicker' 36 | ) { 37 | return true; 38 | } 39 | } else if (pageUrl.startsWith("https://world.hey.com/dhh/")) { 40 | if (node.classList.contains('bio') || 41 | node.classList.contains('push_double--top') || 42 | node.classList.contains('push_half--bottom') 43 | ) { 44 | return true; 45 | } 46 | } 47 | 48 | return false; 49 | } 50 | 51 | function isNumeric(str) { 52 | return /^\d+$/.test(str); 53 | } 54 | 55 | function checkString(input) { 56 | const regex = /^[\d.,!?;:-\s]+$/ 57 | return regex.test(input); 58 | } 59 | 60 | function getPageNodes() { 61 | var pageUrl = window.location.href; 62 | if (pageUrl.startsWith("https://github.com")) { 63 | if (document.querySelector("readme-toc")) { 64 | return getTextNodes(document.querySelector("readme-toc")); 65 | } else if (document.querySelector(".application-main")) { 66 | return getTextNodes(document.querySelector(".application-main")); 67 | } else { 68 | return getTextNodes(document.body); 69 | } 70 | } else if (pageUrl.startsWith("https://www.reddit.com")) { 71 | if (document.querySelector('[data-testid="post-container"]')) { 72 | var containerNode = document.querySelector('[data-testid="post-container"]'); 73 | return getTextNodes(containerNode.parentNode); 74 | } else { 75 | return getTextNodes(document.body); 76 | } 77 | } else if (pageUrl.startsWith("https://gitlab.com")) { 78 | if (document.querySelector("article")) { 79 | return getTextNodes(document.querySelector("article")); 80 | } 81 | } else { 82 | return getTextNodes(document.body); 83 | } 84 | } 85 | 86 | function addTranslations() { 87 | const textNodes = getPageNodes(); 88 | var pageUrl = window.location.href; 89 | 90 | let index = 0; 91 | let nodeTexts = []; 92 | for (const textNode of textNodes) { 93 | const textContent = textNode.textContent; 94 | const text = textContent.trim(); 95 | 96 | if (text.length === 0 || 97 | text.length === 1 || 98 | checkString(textContent) || 99 | (["nil"].includes(text)) || 100 | textNode.parentNode.tagName === 'CODE' || 101 | textNode.parentNode.tagName === 'PRE' || 102 | textNode.parentNode.tagName === 'BUTTON') { 103 | continue; 104 | } 105 | 106 | if (pageUrl.startsWith("https://www.reddit.com") && 107 | (isNumeric(textContent) || 108 | textContent.startsWith("/r/") || 109 | textContent.startsWith("/u/") || 110 | textContent.startsWith("r/") || 111 | textContent.startsWith("u/") || 112 | textContent.startsWith("level ") || 113 | textContent.endsWith(" ago") || 114 | (["give award", "award", "share", "reply", "cc", "comment as", "posted by", "op", 115 | "report", "save", "follow", "markdown mode", "continue this thread"].includes(text.toLowerCase())) || 116 | (textNode.className && textNode.className.includes("button")) || 117 | (textNode.className && textNode.className.includes("icon-comment")) 118 | )) { 119 | continue; 120 | } 121 | 122 | const translatedText = "eaf-translated-node-" + index; 123 | const translatedTextNode = document.createTextNode(""); 124 | const translatedNode = document.createElement("div"); 125 | 126 | translatedNode.appendChild(translatedTextNode); 127 | translatedNode.classList.add("eaf-translated"); 128 | translatedNode.classList.add(translatedText); 129 | 130 | textNode.after(translatedNode); 131 | 132 | nodeTexts.push(textContent); 133 | 134 | index++; 135 | } 136 | 137 | console.log("##### ", nodeTexts); 138 | 139 | return nodeTexts; 140 | } 141 | 142 | return addTranslations(); 143 | })(); 144 | -------------------------------------------------------------------------------- /extension/eaf-all-the-icons.el: -------------------------------------------------------------------------------- 1 | ;;; eaf-all-the-icons.el --- Emacs application framework -*- lexical-binding: t -*- 2 | 3 | ;; Filename: eaf-all-the-icons.el 4 | ;; Description: Emacs application framework 5 | ;; Author: lhpfvs 6 | ;; Maintainer: Andy Stewart 7 | ;; URL: https://github.com/emacs-eaf/emacs-application-framework 8 | ;; Keywords: 9 | ;; Compatibility: emacs-version >= 27 10 | ;; 11 | ;; Features that might be required by this library: 12 | ;; 13 | ;; Please check README 14 | ;; 15 | 16 | ;; This file is not part of GNU Emacs 17 | 18 | ;; This file is free software; you can redistribute it and/or modify 19 | ;; it under the terms of the GNU General Public License as published by 20 | ;; the Free Software Foundation; either version 3, or (at your option) 21 | ;; any later version. 22 | 23 | ;; This program is distributed in the hope that it will be useful, 24 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 25 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26 | ;; GNU General Public License for more details. 27 | 28 | ;; For a full copy of the GNU General Public License 29 | ;; see . 30 | 31 | ;;; Code: 32 | 33 | (require 'all-the-icons) 34 | 35 | (defvar eaf-all-the-icons-alist 36 | '(("EAF/browser" all-the-icons-faicon "chrome" :v-adjust -0.1 :face all-the-icons-lblue) 37 | ("EAF/pdf-viewer" all-the-icons-octicon "file-pdf" :v-adjust 0.0 :face all-the-icons-dred) 38 | ("EAF/image-viewer" all-the-icons-octicon "file-media" :v-adjust 0.0 :face all-the-icons-orange) 39 | ("EAF/markdown-previewer" all-the-icons-octicon "markdown" :v-adjust 0.0 :face all-the-icons-lblue) 40 | ("EAF/video-player" all-the-icons-faicon "film" :face all-the-icons-blue) 41 | ("EAF/camera" all-the-icons-faicon "camera-retro" :height 1.0 :v-adjust -0.1) 42 | ("EAF/music-player" all-the-icons-faicon "music" :height 1.0 :v-adjust -0.1) 43 | ("EAF/terminal" all-the-icons-faicon "terminal" :v-adjust 0.2) 44 | ("EAF/org-previewer" all-the-icons-fileicon "org" :face all-the-icons-lgreen) 45 | ("EAF/mindmap" all-the-icons-alltheicon "html5" :face all-the-icons-orange) 46 | ("EAF/demo" all-the-icons-alltheicon "html5" :face all-the-icons-orange) 47 | ("EAF/vue-demo" all-the-icons-alltheicon "html5" :face all-the-icons-orange) 48 | ("EAF/file-sender" all-the-icons-octicon "file-directory" :v-adjust 0.0) 49 | ("EAF/file-manager" all-the-icons-octicon "file-directory" :v-adjust 0.0) 50 | ("EAF/file-receiver" all-the-icons-octicon "file-directory" :v-adjust 0.0) 51 | ("EAF/airshare" all-the-icons-octicon "file-directory" :v-adjust 0.0) 52 | ("EAF/jupyter" all-the-icons-fileicon "jupyter" :height 1.0 :face all-the-icons-dorange))) 53 | 54 | (defun eaf-all-the-icons-icon (mode-name &rest arg-overrides) 55 | (let* ((icon (all-the-icons-match-to-alist mode-name eaf-all-the-icons-alist)) 56 | (args (cdr icon))) 57 | (when arg-overrides (setq args (append `(,(car args)) arg-overrides (cdr args)))) 58 | (if (and (car icon) args) 59 | (apply (car icon) args) 60 | (message (concat "[" mode-name "] all-the-icons not specified!"))))) 61 | 62 | (when (require 'all-the-icons-ibuffer nil 'noerror) 63 | (define-ibuffer-column icon 64 | (:name " " :inline t) 65 | (let ((icon (cond ((and (buffer-file-name) (all-the-icons-auto-mode-match?)) 66 | (all-the-icons-icon-for-file (file-name-nondirectory (buffer-file-name)) 67 | :height all-the-icons-ibuffer-icon-size 68 | :v-adjust all-the-icons-ibuffer-icon-v-adjust)) 69 | ((eq major-mode 'eaf-mode) 70 | (eaf-all-the-icons-icon mode-name 71 | :height all-the-icons-ibuffer-icon-size 72 | :v-adjust all-the-icons-ibuffer-icon-v-adjust)) 73 | (t 74 | (all-the-icons-icon-for-mode major-mode 75 | :height all-the-icons-ibuffer-icon-size 76 | :v-adjust all-the-icons-ibuffer-icon-v-adjust))))) 77 | (if (or (null icon) (symbolp icon)) 78 | (setq icon (all-the-icons-faicon "file-o" 79 | :face (if all-the-icons-ibuffer-color-icon 80 | 'all-the-icons-dsilver 81 | 'all-the-icons-ibuffer-icon-face) 82 | :height (* 0.9 all-the-icons-ibuffer-icon-size) 83 | :v-adjust all-the-icons-ibuffer-icon-v-adjust)) 84 | (let* ((props (get-text-property 0 'face icon)) 85 | (family (plist-get props :family)) 86 | (face (if all-the-icons-ibuffer-color-icon 87 | (or (plist-get props :inherit) props) 88 | 'all-the-icons-ibuffer-icon-face)) 89 | (new-face `(:inherit ,face 90 | :family ,family 91 | :height ,all-the-icons-ibuffer-icon-size))) 92 | (propertize icon 'face new-face)))))) 93 | 94 | (defun eaf-all-the-icons-update-icon () 95 | "You should custom modeline icon with (eaf-all-the-icons-icon mode-name). 96 | 97 | This function nothing to do default.") 98 | 99 | (eval-when-compile 100 | (when (require 'all-the-icons-ivy-rich nil 'noerror) 101 | (defun eaf-all-the-icons-ivy-rich (candidate) 102 | "Add EAF buffer icon for `ivy-rich'." 103 | (let* ((buffer (get-buffer candidate)) 104 | (buffer-file-name (buffer-file-name buffer)) 105 | (major-mode (buffer-local-value 'major-mode buffer)) 106 | (icon (with-current-buffer buffer (if (eq major-mode 'eaf-mode) 107 | (eaf-all-the-icons-icon mode-name) 108 | (all-the-icons-icon-for-buffer))))) 109 | (all-the-icons-ivy-rich--format-icon 110 | (if (or (null icon) (symbolp icon)) 111 | (all-the-icons-faicon "file-o" :face 'all-the-icons-dsilver :height 0.9 :v-adjust 0.0) 112 | (propertize icon 'display '(raise 0.0)))))) 113 | (advice-add #'all-the-icons-ivy-rich-buffer-icon :override #'eaf-all-the-icons-ivy-rich))) 114 | 115 | (provide 'eaf-all-the-icons) 116 | 117 | ;;; eaf-all-the-icons.el ends here 118 | -------------------------------------------------------------------------------- /sync-eaf-resources.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import os 5 | import sys 6 | import subprocess 7 | import json 8 | import tempfile 9 | 10 | script_path = os.path.dirname(os.path.realpath(__file__)) 11 | 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument("--force", action="store_true", 14 | help='force sync even when there is no updates') 15 | parser.add_argument("--really-run", action="store_true", 16 | help='Really run this script.') 17 | parser.add_argument("--mirror-username", type=str, 18 | help='The username of mirror.') 19 | parser.add_argument("--mirror-password", type=str, 20 | help='The password or token of mirror.') 21 | parser.add_argument("--mirror-use-ssh", action="store_true", 22 | help='push to mirror by ssh url, which can be run without password.') 23 | args = parser.parse_args() 24 | 25 | def run_command(command, path=script_path, ensure_pass=True, get_result=False, print_command=True): 26 | if print_command: 27 | print("[EAF] Running", ' '.join(command), "@", path) 28 | 29 | # Use LC_ALL=C to make sure command output use English. 30 | # Then we can use English keyword to check command output. 31 | english_env = os.environ.copy() 32 | english_env['LC_ALL'] = 'C' 33 | 34 | if get_result: 35 | process = subprocess.Popen(command, env = english_env,stdin = subprocess.PIPE, 36 | stdout = subprocess.PIPE, universal_newlines=True, 37 | text=True, cwd=path) 38 | else: 39 | process = subprocess.Popen(command, env = english_env, stdin = subprocess.PIPE, 40 | universal_newlines=True, text=True, cwd=path) 41 | process.wait() 42 | if process.returncode != 0 and ensure_pass: 43 | sys.exit(process.returncode) 44 | if get_result: 45 | return process.stdout.readlines() 46 | else: 47 | return None 48 | 49 | def yes_no(question, default_yes=False, default_no=False): 50 | key = input(question) 51 | if default_yes: 52 | return key.lower() == 'y' or key == "" 53 | elif default_no: 54 | return key.lower() == 'y' or not (key == "" or key.lower() == 'n') 55 | else: 56 | return key.lower() == 'y' 57 | 58 | def add_auth_info_to_url(url, username, password): 59 | url = url or "" 60 | new_url = str.replace(url, "https://gitee.com", "https://{0}:{1}@gitee.com".format(username, password)) 61 | if url == new_url: # Fail to insert auth info into url. 62 | return False 63 | else: 64 | return new_url 65 | 66 | def convert_https_url_to_ssh(url): 67 | url = url or "" 68 | new_url = str.replace(url, "https://gitee.com/", "git@gitee.com:") 69 | if url == new_url: # Fail to convert https url to ssh url. 70 | return False 71 | else: 72 | return new_url 73 | 74 | def git_repos_sync(mirror_username, mirror_password, mirror_use_ssh): 75 | with open(os.path.join(script_path, 'applications.json')) as f: 76 | app_dict = json.load(f) 77 | for app_name, app_spec_dict in app_dict.items(): 78 | path = os.path.join(tempfile.gettempdir(), "sync-eaf-resourcs", app_name) 79 | branch = app_spec_dict["branch"] 80 | url = app_spec_dict["url"] 81 | if "mirror_url" in app_spec_dict: 82 | mirror_url = app_spec_dict["mirror_url"] 83 | if mirror_use_ssh: 84 | mirror_url_with_auth_info = convert_https_url_to_ssh(mirror_url) 85 | else: 86 | mirror_url_with_auth_info = add_auth_info_to_url(mirror_url, mirror_username, mirror_password) 87 | updated = True 88 | print("[EAF] * Sync EAF {0} repo.".format(app_name)) 89 | if url and mirror_url_with_auth_info: 90 | print("[EAF] ** Upstream -> Local-dir") 91 | if os.path.exists(path): 92 | run_command(["git", "clean", "-df"], path=path) 93 | run_command(["git", "remote", "rm", "origin"], path=path) 94 | run_command(["git", "remote", "add", "origin", url], path=path) 95 | run_command(["git", "reset", "--hard"], path=path) 96 | output_lines = run_command(["git", "pull", "origin", branch], path=path, ensure_pass=False, get_result=True) 97 | for output in output_lines: 98 | print(output) 99 | if "Already up to date." in output: 100 | updated = False 101 | else: 102 | run_command(["git", "clone", "--branch", branch, url, path]) 103 | 104 | if updated or args.force: 105 | print("[EAF] ** Local-dir -> Mirror") 106 | print("[EAF] Running git push -f ") 107 | run_command(["git", "push", "-f", mirror_url_with_auth_info], path=path, print_command=False, get_result=True, ensure_pass=False) 108 | else: 109 | print("[EAF] WARN: url or mirror_url of EAF {} may have some problem, please check them!".format(app_name)) 110 | 111 | def main(): 112 | try: 113 | if args.really_run: 114 | ## Before push to mirror url, username and password of mirror 115 | ## will be inserted into this url. 116 | ## 117 | ## Username and password of mirror come from arguments or 118 | ## environment variable: 119 | ## 120 | ## 1. EAF_MIRROR_USERNAME 121 | ## 2. EAF_MIRROR_PASSWORD 122 | ## 123 | ## In github-action platform, they can be configed in: 124 | ## "https://github.com" -> "Settings" -> "Secrets" -> "New repository secret" 125 | result = True 126 | try: 127 | mirror_username = args.mirror_username or os.environ["EAF_MIRROR_USERNAME"] 128 | mirror_password = args.mirror_password or os.environ["EAF_MIRROR_PASSWORD"] 129 | except KeyError: 130 | mirror_username = False 131 | mirror_password = False 132 | else: 133 | print("[EAF] Do nothing, exiting...") 134 | sys.exit() 135 | 136 | if args.mirror_use_ssh: 137 | result = True 138 | mirror_use_ssh = True 139 | else: 140 | mirror_use_ssh = False 141 | if mirror_username and mirror_password and len(mirror_username) > 0 and len(mirror_password) > 0: 142 | result = True 143 | else: 144 | result = False 145 | print("[EAF] No username or password of mirror, exiting...") 146 | sys.exit() 147 | 148 | if result: 149 | print("[EAF] sync-eaf-resources.py started") 150 | print("[EAF] -----------------------------\n") 151 | git_repos_sync(mirror_username, mirror_password, mirror_use_ssh) 152 | print("\n[EAF] -----------------------------") 153 | else: 154 | sys.exit() 155 | print("[EAF] sync-eaf-resources.py finished!") 156 | except KeyboardInterrupt: 157 | print("[EAF] sync-eaf-resources.py aborted!") 158 | sys.exit() 159 | 160 | 161 | if __name__ == '__main__': 162 | main() 163 | -------------------------------------------------------------------------------- /applications.json: -------------------------------------------------------------------------------- 1 | { 2 | "emacs-application-framework": { 3 | "name": "EAF (Emacs Application Framework)", 4 | "desc": "EAF core repo", 5 | "type": "core", 6 | "branch": "master", 7 | "url": "https://github.com/emacs-eaf/emacs-application-framework.git", 8 | "default_install": "true" 9 | }, 10 | "browser": { 11 | "name": "EAF Browser", 12 | "desc": "A modern, customizable and extensible browser in Emacs", 13 | "type": "app", 14 | "branch": "master", 15 | "url": "https://github.com/emacs-eaf/eaf-browser.git", 16 | "default_install": "true" 17 | }, 18 | "pdf-viewer": { 19 | "name": "EAF PDF Viewer", 20 | "desc": "Fastest PDF Viewer in Emacs", 21 | "type": "app", 22 | "branch": "master", 23 | "url": "https://github.com/emacs-eaf/eaf-pdf-viewer.git", 24 | "default_install": "true" 25 | }, 26 | "music-player": { 27 | "name": "EAF Music Player", 28 | "desc": "Music player that supports playlist and audio visualization", 29 | "type": "app", 30 | "branch": "master", 31 | "url": "https://github.com/emacs-eaf/eaf-music-player.git", 32 | "default_install": "false" 33 | }, 34 | "video-player": { 35 | "name": "EAF Video Player", 36 | "desc": "Video Player in Emacs", 37 | "type": "app", 38 | "branch": "master", 39 | "url": "https://github.com/emacs-eaf/eaf-video-player.git", 40 | "default_install": "false" 41 | }, 42 | "js-video-player": { 43 | "name": "EAF Video Player (JS)", 44 | "desc": "Video Player in Emacs, build by Vue.js", 45 | "type": "app", 46 | "branch": "master", 47 | "url": "https://github.com/emacs-eaf/eaf-js-video-player.git", 48 | "default_install": "false" 49 | }, 50 | "image-viewer": { 51 | "name": "EAF Image Viewer", 52 | "desc": "Dynanmic image viewer", 53 | "type": "app", 54 | "branch": "master", 55 | "url": "https://github.com/emacs-eaf/eaf-image-viewer.git", 56 | "default_install": "false" 57 | }, 58 | "rss-reader": { 59 | "name": "EAF RSS Reader", 60 | "desc": "RSS Reader in Emacs", 61 | "type": "app", 62 | "branch": "master", 63 | "url": "https://github.com/emacs-eaf/eaf-rss-reader.git", 64 | "default_install": "false" 65 | }, 66 | "terminal": { 67 | "name": "EAF Terminal", 68 | "desc": "Full-featured terminal in Emacs, pyqterminal is faster, please install pyqterminal", 69 | "type": "app", 70 | "branch": "master", 71 | "url": "https://github.com/emacs-eaf/eaf-terminal.git", 72 | "default_install": "false" 73 | }, 74 | "markdown-previewer": { 75 | "name": "EAF Markdown Previewer", 76 | "desc": "Real-time Markdown previewer", 77 | "type": "app", 78 | "branch": "master", 79 | "url": "https://github.com/emacs-eaf/eaf-markdown-previewer.git", 80 | "default_install": "false" 81 | }, 82 | "org-previewer": { 83 | "name": "EAF Org Previewer", 84 | "desc": "Real-time Org-mode previewer", 85 | "type": "app", 86 | "branch": "master", 87 | "url": "https://github.com/emacs-eaf/eaf-org-previewer.git", 88 | "default_install": "false" 89 | }, 90 | "camera": { 91 | "name": "EAF Camera", 92 | "desc": "Camera in Emacs", 93 | "type": "app", 94 | "branch": "master", 95 | "url": "https://github.com/emacs-eaf/eaf-camera.git", 96 | "default_install": "false" 97 | }, 98 | "git": { 99 | "name": "EAF Git", 100 | "desc": "Fully multi-threaded git client for Emacs", 101 | "type": "app", 102 | "branch": "master", 103 | "url": "https://github.com/emacs-eaf/eaf-git.git", 104 | "default_install": "false" 105 | }, 106 | "file-manager": { 107 | "name": "EAF File Manager", 108 | "desc": "Fully multi-threaded replacement for dired-mode", 109 | "type": "app", 110 | "branch": "master", 111 | "url": "https://github.com/emacs-eaf/eaf-file-manager.git", 112 | "default_install": "false" 113 | }, 114 | "mindmap": { 115 | "name": "EAF Mindmap", 116 | "desc": "Keyboard-driven Mindmap editor", 117 | "type": "app", 118 | "branch": "master", 119 | "url": "https://github.com/emacs-eaf/eaf-mindmap.git", 120 | "default_install": "false" 121 | }, 122 | "mind-elixir": { 123 | "name": "EAF Mindmap base on Mind Elixir", 124 | "desc": "Keyboard-driven Mindmap editor", 125 | "type": "app", 126 | "branch": "master", 127 | "url": "https://github.com/emacs-eaf/eaf-mind-elixir.git", 128 | "default_install": "false" 129 | }, 130 | "system-monitor": { 131 | "name": "EAF System Monitor", 132 | "desc": "Simple system monitor tool", 133 | "type": "app", 134 | "branch": "master", 135 | "url": "https://github.com/emacs-eaf/eaf-system-monitor.git", 136 | "default_install": "false" 137 | }, 138 | "file-browser": { 139 | "name": "EAF File Browser", 140 | "desc": "Browse computer files on your phone", 141 | "type": "app", 142 | "branch": "master", 143 | "url": "https://github.com/emacs-eaf/eaf-file-browser.git", 144 | "default_install": "false" 145 | }, 146 | "file-sender": { 147 | "name": "EAF File Sender", 148 | "desc": "Share file between Emacs and mobile phone", 149 | "type": "app", 150 | "branch": "master", 151 | "url": "https://github.com/emacs-eaf/eaf-file-sender.git", 152 | "default_install": "false" 153 | }, 154 | "airshare": { 155 | "name": "EAF Airshare", 156 | "desc": "Share text between Emacs and your phone", 157 | "type": "app", 158 | "branch": "master", 159 | "url": "https://github.com/emacs-eaf/eaf-airshare.git", 160 | "default_install": "false" 161 | }, 162 | "jupyter": { 163 | "name": "EAF Jupyter", 164 | "desc": "Jupyter client", 165 | "type": "app", 166 | "branch": "master", 167 | "url": "https://github.com/emacs-eaf/eaf-jupyter.git", 168 | "default_install": "false" 169 | }, 170 | "2048": { 171 | "name": "EAF 2048", 172 | "desc": "An 2048 game in Emacs", 173 | "type": "app", 174 | "branch": "master", 175 | "url": "https://github.com/metaescape/2048pyqt6.git", 176 | "default_install": "false" 177 | }, 178 | "markmap": { 179 | "name": "EAF markmap", 180 | "desc": "Visualize your Markdown as mindmaps", 181 | "type": "app", 182 | "branch": "master", 183 | "url": "https://github.com/emacs-eaf/eaf-markmap.git", 184 | "default_install": "false" 185 | }, 186 | "map": { 187 | "name": "EAF Map", 188 | "desc": "EAF OpenStreetMap application for EAF", 189 | "type": "app", 190 | "branch": "master", 191 | "url": "https://github.com/emacs-eaf/eaf-map.git", 192 | "default_install": "false" 193 | }, 194 | "demo": { 195 | "name": "EAF Demo", 196 | "desc": "EAF app demo based on PyQt", 197 | "type": "app", 198 | "branch": "master", 199 | "url": "https://github.com/emacs-eaf/eaf-demo.git", 200 | "default_install": "false" 201 | }, 202 | "vue-demo": { 203 | "name": "EAF Vue Demo", 204 | "desc": "EAF app demo base on Vue.js", 205 | "type": "app", 206 | "branch": "master", 207 | "url": "https://github.com/emacs-eaf/eaf-vue-demo.git", 208 | "default_install": "false" 209 | }, 210 | "vue-tailwindcss": { 211 | "name": "EAF Vue Tailwindcss Demo", 212 | "desc": "EAF app tailwindcss demo base on Vue.js", 213 | "type": "app", 214 | "branch": "master", 215 | "url": "https://github.com/emacs-eaf/eaf-vue-tailwindcss.git", 216 | "default_install": "false" 217 | }, 218 | "pyqterminal": { 219 | "name": "EAF PyQterminal", 220 | "desc": "A terminal written in PyQt6", 221 | "type": "app", 222 | "branch": "main", 223 | "url": "https://github.com/mumu-lhl/eaf-pyqterminal.git", 224 | "default_install": "false" 225 | }, 226 | "video-editor": { 227 | "name": "EAF Video Editor", 228 | "desc": "A simple video editor", 229 | "type": "app", 230 | "branch": "main", 231 | "url": "https://github.com/ginqi7/eaf-video-editor.git", 232 | "default_install": "false" 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /extension/eaf-org.el: -------------------------------------------------------------------------------- 1 | ;;; eaf-org.el --- Emacs application framework -*- lexical-binding: t; -*- 2 | 3 | ;; Filename: eaf-org.el 4 | ;; Description: Emacs application framework 5 | ;; Author: stardiviner 6 | ;; Maintainer: Andy Stewart 7 | ;; Copyright (C) 2018, Andy Stewart, all rights reserved. 8 | ;; Created: 2020-05-17 12:31:12 9 | ;; Version: 0.5 10 | ;; Last-Updated: Wed Sep 8 12:00:50 2021 (-0400) 11 | ;; By: Mingde (Matthew) Zeng 12 | ;; URL: https://github.com/emacs-eaf/emacs-application-framework 13 | ;; Keywords: 14 | ;; Compatibility: emacs-version >= 27 15 | ;; 16 | ;; Features that might be required by this library: 17 | ;; 18 | ;; Please check README 19 | ;; 20 | 21 | ;;; This file is NOT part of GNU Emacs 22 | 23 | ;;; License 24 | ;; 25 | ;; This program is free software; you can redistribute it and/or modify 26 | ;; it under the terms of the GNU General Public License as published by 27 | ;; the Free Software Foundation; either version 3, or (at your option) 28 | ;; any later version. 29 | 30 | ;; This program is distributed in the hope that it will be useful, 31 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | ;; GNU General Public License for more details. 34 | 35 | ;; You should have received a copy of the GNU General Public License 36 | ;; along with this program; see the file COPYING. If not, write to 37 | ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth 38 | ;; Floor, Boston, MA 02110-1301, USA. 39 | 40 | ;;; Code: 41 | 42 | (if (version< emacs-version "27") 43 | (require 'org-docview) 44 | (require 'ol)) 45 | 46 | (defcustom eaf-org-override-pdf-links-open nil 47 | "When enabled, this will override existing PDF file links's open function. 48 | 49 | So that every existing PDF org-link that's supposed to be opened 50 | by something in `eaf-org-override-pdf-links-list' will be opened using EAF. 51 | 52 | Enable this when the you want to ensure the PDF link in the org file can be 53 | opened without EAF enabled." 54 | :type 'boolean 55 | :safe #'booleanp 56 | :group 'org-link) 57 | 58 | (defcustom eaf-org-override-pdf-links-store nil 59 | "When enabled, PDF link types will store as eaf:pdfviewer: link type." 60 | :type 'boolean 61 | :safe #'booleanp 62 | :group 'org-link) 63 | 64 | (defun eaf-org-export-to-pdf-and-open () 65 | "Run `org-latex-export-to-pdf', delete the tex file and `eaf-open' pdf in a new buffer." 66 | (interactive) 67 | (with-current-buffer (current-buffer) 68 | (when (derived-mode-p 'org-mode) 69 | (save-buffer) 70 | (let* ((export (org-latex-export-to-pdf)) 71 | (pdf-name (file-name-nondirectory (file-name-sans-extension (buffer-file-name)))) 72 | (pdf-name-with-ext (concat pdf-name ".pdf")) 73 | (eaf-pdf-buffer (get-buffer pdf-name-with-ext)) 74 | (pdf-full-path (concat (file-name-directory (buffer-file-name)) pdf-name-with-ext))) 75 | (when export 76 | (message (concat "Trying to open " pdf-name-with-ext)) 77 | (delete-file (concat pdf-name ".tex")) 78 | (delete-other-windows) 79 | (split-window-right) 80 | (other-window 1) 81 | (when (get-buffer pdf-name-with-ext) 82 | (kill-buffer pdf-name-with-ext)) 83 | (eaf-open pdf-full-path)))))) 84 | 85 | (defvar eaf-org-override-pdf-links-list 86 | '("docview" "pdfview" "pdftools") 87 | "A list of all PDF file link types which will be override by EAF open function.") 88 | 89 | (dolist (type eaf-org-override-pdf-links-list) 90 | (when (and eaf-org-override-pdf-links-open 91 | (org-link-get-parameter type :follow)) ; if `nil' means `ol-' not loaded. 92 | (org-link-set-parameters ; store original `:follow' function 93 | type :orig-follow (org-link-get-parameter type :follow)) 94 | (org-link-set-parameters type :follow #'eaf-org-open))) 95 | 96 | (defun eaf-org-store-link () 97 | "Store the page of PDF as link support for `org-store-link'. 98 | The raw link looks like this: [[eaf:::::][description]]" 99 | (interactive) 100 | (when (eq major-mode 'eaf-mode) 101 | (let* ((app eaf--buffer-app-name) 102 | ;; filter temp files which is converted to PDF 103 | (url (if (string-prefix-p "/tmp/" eaf--buffer-url) 104 | (warn "[EAF] doesn't support this application link which is converted to temporary PDF file.") 105 | eaf--buffer-url)) 106 | (extra-args (cl-case (intern app) 107 | ('pdf-viewer 108 | (eaf-call-sync "execute_function" eaf--buffer-id "current_page")) 109 | ('js-video-player 110 | (eaf-call-sync "execute_function" eaf--buffer-id "save_session_data")))) 111 | (link (if eaf-org-override-pdf-links-store 112 | (if extra-args 113 | (concat "eaf:" app "::" url "::" extra-args) 114 | (concat "eaf:" app "::" url)) 115 | (if extra-args 116 | (concat url "::" extra-args) 117 | (concat url)))) 118 | (description (buffer-name))) 119 | (pcase app 120 | ("pdf-viewer" 121 | (when eaf-org-override-pdf-links-open 122 | (or (equal (org-link-get-parameter "docview" :follow) 'eaf-org-open) 123 | (equal (org-link-get-parameter "pdfview" :follow) 'eaf-org-open) 124 | (equal (org-link-get-parameter "pdftools" :follow) 'eaf-org-open))) 125 | (if eaf-org-override-pdf-links-store 126 | (org-link-store-props 127 | :type "eaf" 128 | :link link 129 | :description description) 130 | (require 'ol-docview) ; use `docview' for most wide compatible support. 131 | (org-link-store-props 132 | :type "docview" 133 | :link (concat "docview:" link) 134 | :description description))) 135 | (_ (org-link-store-props 136 | :type "eaf" 137 | :link link 138 | :description description)))))) 139 | 140 | (defun eaf-org-open (link &optional _) 141 | "Open LINK using EAF on an EAF supported file." 142 | (if (member (car (split-string link "::")) (mapcar 'car eaf-app-extensions-alist)) 143 | ;; for eaf-org link type spec: "eaf:::::" 144 | (let* ((list (split-string link "::")) 145 | (app (car list)) 146 | (url (cadr list)) 147 | (extra-args (caddr list))) 148 | (cl-case (intern app) 149 | ('browser 150 | (eaf-open url "browser")) 151 | ('pdf-viewer 152 | (eaf-open url "pdf-viewer") 153 | (eaf-call-sync "execute_function_with_args" eaf--buffer-id 154 | "jump_to_page_with_num" (format "%s" extra-args))) 155 | ('mindmap 156 | (eaf-open url "mindmap")) 157 | ('js-video-player 158 | (eaf-open url "js-video-player") 159 | (eaf-call-sync "execute_function_with_args" eaf--buffer-id 160 | "restore_session_data" (format "%s" extra-args))) 161 | (t (eaf-open url)))) 162 | ;; for other link types spec: ":URL:(parameters)" 163 | ;; NOTE: currently only support override PDF link types. 164 | (let* ((list (split-string link "::")) 165 | (url (car list)) 166 | (extra-args (cadr list))) 167 | (cl-case (intern (file-name-extension url)) 168 | ('pdf 169 | (if eaf-org-override-pdf-links-open 170 | (progn (eaf-open (expand-file-name url) "pdf-viewer") 171 | (when extra-args 172 | (eaf-call-sync "execute_function_with_args" eaf--buffer-id 173 | "jump_to_page_with_num" (format "%s" extra-args)))) 174 | (dolist (type eaf-org-override-pdf-links-list) 175 | ;; restore to original :follow function, since eaf-org-override-pdf-links-open is nil 176 | (org-link-set-parameters 177 | type :follow (org-link-get-parameter type :orig-follow)) 178 | ;; re-open link with original :follow function 179 | (apply (org-link-get-parameter type :follow) link)))))))) 180 | 181 | (org-link-set-parameters "eaf" 182 | :follow #'eaf-org-open 183 | :store #'eaf-org-store-link) 184 | 185 | 186 | (provide 'eaf-org) 187 | ;;; eaf-org.el ends here 188 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) | 简体中文 2 | 3 |

4 | 5 |
新一代的 Emacs 图形应用框架, 通过扩展 Emacs 的多媒体能力, 达到 Live in Emacs 的终极目标 6 |

7 | 8 | ## 愿景 9 | 10 | 11 | Emacs 距今已经有 45 年的发展历史, 比现在人们用的操作系统都老。 在这 45 年中, 全世界最顶级的黑客在贡献自己的智慧和想象力, 一起构建了 Emacs 这个伟大的开发者工具生态。 12 | 13 | 当你是一个需要使用十几门编程语言的黑客和键盘流信仰者, Emacs 绝对是你的不二之选。 14 | 15 | Emacs 的劣势也是因为它太古老了, 导致在多线程和图形扩展能力已经无法跟上时代的步伐, 在很多地方发展落后于 IDEA 和 VSCode。 16 | 17 | Emacs Application Framework (EAF)的愿景是在保留 Emacs 古老的黑客文化和庞大的开发者插件生态前提下, 通过 EAF 框架扩展 Emacs 的多线程和图形渲染能力, 实现 Live In Emacs 的理想。 18 | 19 | ## EAF 有哪些功能? 20 | EAF 是一个可编程扩展的框架, 它自带一系列丰富的应用, 你可以自由选择哪些下载: 21 | 22 | | 浏览器 | PDF 阅读器 | 23 | | :--------: | :----: | 24 | | | | 25 | 26 | | 音乐播放器 | 文件管理器 | 27 | | :--------: | :----: | 28 | | | | 29 | | | | 30 | 31 | - [Browser](https://github.com/emacs-eaf/eaf-browser): 全功能的网页浏览器, 基于 Chromium 渲染引擎 32 | - [PDF Viewer](https://github.com/emacs-eaf/eaf-pdf-viewer): Emacs 里面渲染速度最快的 PDF 查看器 33 | - [Music Player](https://github.com/emacs-eaf/eaf-music-player): 音乐播放器, 支持播放列表对齐渲染和实时音频反馈 34 | - [Video Player](https://github.com/emacs-eaf/eaf-video-player): 基于 Qt 的视频播放器 35 | - [Image Viewer](https://github.com/emacs-eaf/eaf-image-viewer): 支持实时缩放的图片查看器 36 | - [RSS Reader](https://github.com/emacs-eaf/eaf-rss-reader): 新闻阅读器, 支持 Html 内容渲染 37 | - [Terminal](https://github.com/mumu-lhl/eaf-pyqterminal): 支持图形绘制的全功能终端模拟器 38 | - [MindMap](https://github.com/emacs-eaf/eaf-mind-elixir): 界面美观的全功能思维导图软件 39 | - [Camera](https://github.com/emacs-eaf/eaf-camera): 摄像头程序 40 | - [Markdown Previewer](https://github.com/emacs-eaf/eaf-markdown-previewer): Markdown 文档实时预览程序, 完美兼容 Github 样式, 支持 Mermaid、 PlantUML、 KaTeX、 MathJax 等内容的渲染 41 | - [Org Previewer](https://github.com/emacs-eaf/eaf-org-previewer): Org 文件实时预览程序, 支持文件实时预览 42 | - [Git Client](https://github.com/emacs-eaf/eaf-git): 多线程 Git 客户端 43 | - [File Manager](https://github.com/emacs-eaf/eaf-file-manager): 多线程文件管理器 44 | - [Video Editor](https://github.com/ginqi7/eaf-video-editor): 视频编辑器 45 | 46 | ... 还有[很多](https://github.com/orgs/emacs-eaf/repositories)! 47 | 48 | ### EAF 在 EmacsConf 49 | 50 | | EmacsConf 2020: 用 EAF 扩展 Emacs 图形应用 | EmacsConf 2021: EAF 2021 更新报告 | 51 | | :--------: | :----: | 52 | | [](https://www.youtube.com/watch?v=HK_f8KTuR0s) | [](https://www.youtube.com/watch?v=bh37zbefZk4) | 53 | | | | 54 | 55 | 56 | ## 安装 57 | 58 | EAF 可以在多个操作系统下工作, 包括 Linux X11, Windows, macOS 和 FreeBSD, 安装方法非常简单。 59 | 60 | 如果你使用的是 Nix、 macOS 或者 Gentoo, 你需要先看一下 [Wiki](https://github.com/emacs-eaf/emacs-application-framework/wiki) 61 | 62 | #### 1. 下载 EAF 63 | 64 | ```Bash 65 | git clone --depth=1 -b master https://github.com/emacs-eaf/emacs-application-framework.git ~/.emacs.d/site-lisp/emacs-application-framework/ 66 | ``` 67 | 68 | #### 2. 安装 EAF 依赖 69 | 70 | 调用 Elisp 函数`M-x eaf-install-and-update`或者手动在 Terminal 跑`install-eaf.py`安装脚本: 71 | 72 | ```Bash 73 | cd emacs-application-framework 74 | chmod +x ./install-eaf.py 75 | ./install-eaf.py 76 | ``` 77 | 78 | `install-eaf.py`脚本有许多有用的选项, 可以通过`--help`查看。 79 | 80 | #### 3. 加载 EAF 核心 81 | 82 | 从这里开始, 你可以把 EAF 加入 Emacs 的 ```load-path```, 然后在 `init.el` 中写入: 83 | 84 | ```Elisp 85 | (add-to-list 'load-path "~/.emacs.d/site-lisp/emacs-application-framework/") 86 | (require 'eaf) 87 | ``` 88 | - 或者, 如果你使用 [use-package](https://github.com/jwiegley/use-package), 下面有一个简单的配置文件供你参考: 89 | 90 | ```Elisp 91 | (use-package eaf 92 | :load-path "~/.emacs.d/site-lisp/emacs-application-framework" 93 | :custom 94 | ; See https://github.com/emacs-eaf/emacs-application-framework/wiki/Customization 95 | (eaf-browser-continue-where-left-off t) 96 | (eaf-browser-enable-adblocker t) 97 | (browse-url-browser-function 'eaf-open-browser) 98 | :config 99 | (defalias 'browse-web #'eaf-open-browser) 100 | (eaf-bind-key scroll_up "C-n" eaf-pdf-viewer-keybinding) 101 | (eaf-bind-key scroll_down "C-p" eaf-pdf-viewer-keybinding) 102 | (eaf-bind-key take_photo "p" eaf-camera-keybinding) 103 | (eaf-bind-key nil "M-q" eaf-browser-keybinding)) ;; unbind, see more in the Wiki 104 | ``` 105 | 106 | #### 4. 加载 EAF 应用 107 | 108 | 你可以用下面的代码来加载一部分 EAF 应用, 比如浏览器、 PDF 阅读器和视频播放器, 更多的应用请查看 [应用列表](https://github.com/emacs-eaf/emacs-application-framework#applications): 109 | 110 | ```Elisp 111 | (require 'eaf-browser) 112 | (require 'eaf-pdf-viewer) 113 | ``` 114 | 115 | #### 5. 下载完成! 116 | 117 | 恭喜, 到这一步你已成功下载好了 EAF! 你可以通过`M-x eaf-open-demo`(前提是你下载了`demo`应用)看看 EAF 是否可以成功运行了。 118 | 119 | 下面是 EAF 应用的启动命令: 120 | 121 | | 应用名称 | 启动命令 | 122 | | :-------- | :---- | 123 | | 浏览器 | `M-x eaf-open-browser` 在浏览器中打开或搜索 | 124 | | | `M-x eaf-open-browser-with-history` 搜索历史或者打开 URL | 125 | | HTML 邮件渲染 | `M-x eaf-open-mail-as-html` 在 `gnus`, `mu4e`, `notmuch` 等邮件客户端中执行 | 126 | | PDF 阅读器 | `M-x eaf-open` 输入 PDF 文件 | 127 | | 视频播放器 | `M-x eaf-open` 输入视频文件 | 128 | | 图片浏览器 | `M-x eaf-open` 输入图片文件 | 129 | | Markdown 预览 | `M-x eaf-open` 输入 Markdown 文件, 选择 markdown-previewer | 130 | | Org 预览 | `M-x eaf-open` 输入 Org 文件, 选择 org-previewer | 131 | | 摄像头程序 | `M-x eaf-open-camera` | 132 | | 终端模拟器 | `M-x eaf-open-pyqterminal` | 133 | | 文件管理器 | `M-x eaf-open-in-file-manager` | 134 | | 新闻阅读器 | `M-x eaf-open-rss-reader` | 135 | | Git 客户端 | `M-x eaf-open-git` | 136 | | 地图路径规划 | `M-x eaf-open-map` | 137 | | 二维码下载文件 | `M-x eaf-file-sender-qrcode` or `eaf-file-sender-qrcode-in-dired` | 138 | | 二维码在线浏览器 | `M-x eaf-file-browser-qrcode` | 139 | | 无线分享 | `M-x eaf-open-airshare` 输入要分享给手机的字符串 | 140 | | Markdown 思维导图预览 | `M-x eaf-open` 输入 Markdown 或 Org 文件, 选择 markmap 141 | | 思维导图 | `M-x eaf-create-mindmap` or `M-x eaf-open-mindmap` | 142 | | 微软 Office 阅读器 | `M-x eaf-open-office` | 143 | | jupyter | `M-x eaf-open-jupyter` | 144 | | 音乐 | `M-x eaf-open-music-player` | 145 | | 系统监视器 | `M-x eaf-open-system-monitor` | 146 | | 演示程序 | `M-x eaf-open-demo` | 147 | | Vue.js 演示程序 | `M-x eaf-open-vue-demo` | 148 | 149 | - EAF 浏览器以及 PDF 浏览器支持 Emacs 内置书签操作, 通过使用`M-x bookmark-set`(默认`C-x r m`)以及`M-x bookmark-bmenu-list`(默认`C-x r l`)。 150 | 151 | ## 更新 152 | 建议你时常`git pull` **并且** 运行`install-eaf.py` (`M-x eaf-install-and-update`)来更新各个 EAF 应用及其依赖。 153 | 154 | ## 反馈问题 155 | 156 | ### 反馈安装和配置问题之前, 请一定先阅读 [Wiki](https://github.com/emacs-eaf/emacs-application-framework/wiki) 以及[常用问题](https://github.com/emacs-eaf/emacs-application-framework/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98)。 157 | 158 | 如果你使用中遇到任何问题, 并且问题是`git pull`后出现的, 请先阅读 [Mandatory Procedures to Keep Your EAF Up-To-Date](https://github.com/emacs-eaf/emacs-application-framework/discussions/527?sort=new) 页面。 159 | 160 | 关于其他问题, 请用命令 `emacs -q` 并只添加 EAF 配置做一个对比测试, 如果 `emacs -q` 可以正常工作, 请检查你个人的配置文件。 161 | 162 | 如果`emacs -q`环境下问题依旧, 请到[这里](https://github.com/emacs-eaf/emacs-application-framework/issues/new) 反馈, 并附带 `*eaf*` 窗口的内容给我们提交 issue, 那里面有很多线索可以帮助我们排查问题。 163 | 164 | 如果你遇到崩溃的问题, 请用下面的方式来收集崩溃信息: 165 | 1. 先安装 gdb 并打开选项 `(setq eaf-enable-debug t)` 166 | 2. 使用命令 `eaf-stop-process` 停止 EAF 进程 167 | 3. 重新打开 EAF, 并在下次崩溃时发送 `*eaf*` 的内容 168 | 169 | ## EAF 社区 170 | 171 | 下面列表列展示了 EAF 在 Emacs 社区的应用。 如果我们遗漏你的应用, 欢迎提交 PR 来加到下面列表中。 172 | 173 | * ***[obr-viz](https://github.com/swhalemwo/obr-viz)***: visualizing [org-brain](https://github.com/Kungsgeten/org-brain) relationships using EAF 174 | * ***[netease-cloud-music](https://github.com/SpringHan/netease-cloud-music.el)***: A netease music client for emacs. 175 | * ***[2048pyqt6](https://github.com/porrige/2048pyqt6)***: A 2048 game that can run in emacs. 176 | * ***[pyqterminal](https://github.com/mumu-lhl/eaf-pyqterminal)***: A terminal written in PyQt6. 177 | 178 | ## 贡献者 179 | 180 | 181 | 182 | 183 | ## 加入我们 184 | 你想把 Emacs 开发成一个操作系统吗? 185 | 186 | 想要在 Emacs 里面生活的更舒适吗? 187 | 188 | 想要创建下一个激动人心的 Emacs 插件吗? 189 | 190 | [一起疯吧!](https://github.com/emacs-eaf/emacs-application-framework/wiki/Hacking) 191 | -------------------------------------------------------------------------------- /core/view.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (C) 2018 Andy Stewart 5 | # 6 | # Author: Andy Stewart 7 | # Maintainer: Andy Stewart 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 3 of the License, or 12 | # any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program. If not, see . 21 | 22 | import platform 23 | 24 | from core.utils import current_desktop, eval_in_emacs, focus_emacs_buffer, get_emacs_func_cache_result, get_emacs_var 25 | from PyQt6.QtCore import QEvent, QPoint, Qt 26 | from PyQt6.QtGui import QBrush, QPainter, QWindow 27 | from PyQt6.QtWidgets import QFrame, QGraphicsView, QVBoxLayout, QWidget 28 | 29 | if current_desktop in ["sway", "Hyprland"] and get_emacs_func_cache_result("eaf-emacs-running-in-wayland-native", []): 30 | global reinput 31 | 32 | import subprocess 33 | 34 | build_dir = get_emacs_var("eaf-build-dir") 35 | reinput_file = build_dir + "reinput/reinput" 36 | pid = get_emacs_func_cache_result("emacs-pid", []) 37 | reinput = subprocess.Popen(f"{reinput_file} {pid}", stdin=subprocess.PIPE, shell=True) 38 | 39 | def focus(): 40 | reinput.stdin.write("1\n".encode("utf-8")) 41 | reinput.stdin.flush() 42 | 43 | def lose_focus(): 44 | reinput.stdin.write("0\n".encode("utf-8")) 45 | reinput.stdin.flush() 46 | 47 | 48 | class View(QWidget): 49 | 50 | def __init__(self, buffer, view_info): 51 | 52 | super(View, self).__init__() 53 | 54 | self.buffer = buffer 55 | 56 | # Init widget attributes. 57 | if get_emacs_func_cache_result("eaf-emacs-running-in-wayland-native", []): 58 | self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.WindowOverridesSystemGestures | Qt.WindowType.BypassWindowManagerHint) 59 | elif get_emacs_func_cache_result("eaf-emacs-not-use-reparent-technology", []): 60 | self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.NoDropShadowWindowHint) 61 | else: 62 | self.setWindowFlags(Qt.WindowType.FramelessWindowHint) 63 | 64 | self.is_member_of_focus_fix_wms = get_emacs_var("eaf-is-member-of-focus-fix-wms") 65 | 66 | self.setAttribute(Qt.WidgetAttribute.WA_X11DoNotAcceptFocus, True) 67 | self.setContentsMargins(0, 0, 0, 0) 68 | self.installEventFilter(self) 69 | 70 | # Init attributes. 71 | self.last_event_type = None 72 | self.view_info = view_info 73 | (self.buffer_id, self.emacs_xid, self.x, self.y, self.width, self.height) = view_info.split(":") 74 | self.x: int = int(self.x) 75 | self.y: int = int(self.y) 76 | self.width: int = int(self.width) 77 | self.height: int = int(self.height) 78 | 79 | # Build QGraphicsView. 80 | self.layout: QVBoxLayout = QVBoxLayout(self) 81 | self.layout.setSpacing(0) 82 | self.layout.setContentsMargins(0, 0, 0, 0) 83 | self.graphics_view = QGraphicsView(buffer, self) 84 | 85 | # Remove border from QGraphicsView. 86 | self.graphics_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 87 | self.graphics_view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 88 | self.graphics_view.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform | QPainter.RenderHint.TextAntialiasing) 89 | self.graphics_view.setFrameStyle(QFrame.Shape.NoFrame) 90 | 91 | # Fill background color. 92 | self.graphics_view.setBackgroundBrush(QBrush(buffer.background_color)) 93 | 94 | # Add graphics view. 95 | self.layout.addWidget(self.graphics_view) 96 | 97 | # NOTE: show function must start before resize to trigger *first* resizeEvent after show. 98 | self.show() 99 | 100 | # Resize after show to trigger fit view operation. 101 | self.resize(self.width, self.height) 102 | 103 | self.buffer.aspect_ratio_change.connect(self.adjust_aspect_ratio) 104 | 105 | self.locate() 106 | 107 | def resizeEvent(self, event): 108 | # Fit content to view rect just when buffer fit_to_view option is enable. 109 | if self.buffer.fit_to_view: 110 | if event.oldSize().isValid(): 111 | self.graphics_view.fitInView(self.graphics_view.scene().sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) 112 | QWidget.resizeEvent(self, event) 113 | 114 | def adjust_aspect_ratio(self): 115 | widget_width = self.width 116 | widget_height = self.height 117 | 118 | if self.buffer.aspect_ratio == 0: 119 | self.buffer.buffer_widget.resize(self.width, self.height) 120 | 121 | self.layout.setContentsMargins(0, 0, 0, 0) 122 | else: 123 | view_height = widget_height * (1 - 2 * self.buffer.vertical_padding_ratio) 124 | view_width = view_height * self.buffer.aspect_ratio 125 | horizontal_padding = (widget_width - view_width) / 2 126 | vertical_padding = self.buffer.vertical_padding_ratio * widget_height 127 | 128 | self.buffer.buffer_widget.resize(int(view_width), int(view_height)) 129 | 130 | self.layout.setContentsMargins(int(horizontal_padding), int(vertical_padding), int(horizontal_padding), int(vertical_padding)) 131 | 132 | def is_switch_from_other_application(self, event): 133 | # When switch to Emacs from other application, such as Alt + Tab. 134 | # 135 | # Event match one of below rules: 136 | return ( 137 | # Current event is QEvent.Type.ShortcutOverride 138 | (event.type() in [QEvent.Type.ShortcutOverride]) or 139 | 140 | # Current event is QEvent.Type.Enter. 141 | ((not self.is_member_of_focus_fix_wms) and 142 | (self.last_event_type not in [QEvent.Type.Resize, QEvent.Type.WinIdChange, QEvent.Type.Leave, QEvent.Type.UpdateRequest]) and 143 | (event.type() in [QEvent.Type.Enter])) or 144 | 145 | # Current event is QEvent.Type.KeyRelease and last event is QEvent.Type.UpdateRequest. 146 | ((not self.is_member_of_focus_fix_wms) and 147 | (self.last_event_type is QEvent.Type.UpdateRequest) and 148 | (event.type() is QEvent.Type.KeyRelease))) 149 | 150 | def eventFilter(self, obj, event): 151 | # ENABLE BELOW CODE FOR DEBUG. 152 | # 153 | # import time 154 | # current_time = time.time() 155 | # print(f"{current_time:.6f}" + " " + event.type().name) 156 | 157 | # Focus emacs window when event type match below event list. 158 | # Make sure EAF window always response user key event after switch from other application, such as Alt + Tab. 159 | if current_desktop in ["sway", "Hyprland"] and get_emacs_func_cache_result("eaf-emacs-running-in-wayland-native", []): 160 | if event.type() == QEvent.Type.WindowActivate: 161 | focus() 162 | elif event.type() == QEvent.Type.WindowDeactivate: 163 | lose_focus() 164 | 165 | if self.is_switch_from_other_application(event): 166 | eval_in_emacs('eaf-activate-emacs-window', [self.buffer_id]) 167 | 168 | # Focus emacs buffer when user click view. 169 | focus_event_types = [QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonRelease, QEvent.Type.MouseButtonDblClick] 170 | if platform.system() != "Darwin": 171 | focus_event_types += [QEvent.Type.Wheel] 172 | 173 | self.last_event_type = event.type() 174 | 175 | if event.type() in focus_event_types: 176 | focus_emacs_buffer(self.buffer_id) 177 | # Stop mouse event. 178 | return True 179 | 180 | return False 181 | 182 | def showEvent(self, event): 183 | # NOTE: we must reparent after widget show, otherwise reparent operation maybe failed. 184 | self.reparent() 185 | 186 | if platform.system() in ["Windows", "Darwin"]: 187 | eval_in_emacs('eaf-activate-emacs-window', []) 188 | 189 | # Make graphics view at left-top corner after show. 190 | self.graphics_view.verticalScrollBar().setValue(0) 191 | self.graphics_view.horizontalScrollBar().setValue(0) 192 | 193 | def reparent(self): 194 | # print("Reparent: ", self.buffer.url) 195 | qwindow = self.windowHandle() 196 | 197 | if not get_emacs_func_cache_result("eaf-emacs-not-use-reparent-technology", []): 198 | qwindow.setParent(QWindow.fromWinId(int(self.emacs_xid))) # type: ignore 199 | 200 | qwindow.setPosition(QPoint(self.x, self.y)) 201 | 202 | def try_show_top_view(self): 203 | if get_emacs_func_cache_result("eaf-emacs-not-use-reparent-technology", []): 204 | self.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True) 205 | self.show() 206 | 207 | def try_hide_top_view(self): 208 | if get_emacs_func_cache_result("eaf-emacs-not-use-reparent-technology", []): 209 | self.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, False) 210 | self.hide() 211 | 212 | def destroy_view(self): 213 | # print("Destroy: ", self.buffer.url) 214 | self.destroy() 215 | 216 | def screen_shot(self): 217 | return self.grab() 218 | 219 | def locate(self): 220 | if not get_emacs_func_cache_result("eaf-emacs-running-in-wayland-native", []): 221 | return 222 | 223 | title = f"eaf.py-{self.x}-{self.y}" 224 | if current_desktop == "Hyprland": 225 | import subprocess 226 | 227 | subprocess.Popen(f"hyprctl --batch 'keyword windowrule float,title:^{title}$;" 228 | f"keyword windowrule move {self.x} {self.y},title:^{title}$'", shell=True) 229 | self.setWindowTitle(title) 230 | elif current_desktop == "sway" and get_emacs_func_cache_result("eaf-emacs-not-use-reparent-technology", []): 231 | import subprocess 232 | 233 | subprocess.Popen(f"swaymsg 'for_window [title={title}] floating enable;" 234 | f"for_window [title={title}] move position {self.x} {self.y}'", shell=True) 235 | self.setWindowTitle(title) 236 | -------------------------------------------------------------------------------- /core/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (C) 2018 Andy Stewart 5 | # 6 | # Author: Andy Stewart 7 | # Maintainer: Andy Stewart 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 3 of the License, or 12 | # any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program. If not, see . 21 | 22 | import os 23 | 24 | import sexpdata 25 | from PyQt6 import QtGui 26 | from PyQt6.QtCore import QObject, pyqtSignal 27 | from PyQt6.QtWidgets import QApplication 28 | 29 | 30 | class PostGui(QObject): 31 | 32 | through_thread = pyqtSignal(object, object) 33 | 34 | def __init__(self, inclass=True): 35 | super(PostGui, self).__init__() 36 | self.through_thread.connect(self.on_signal_received) 37 | self.inclass = inclass 38 | 39 | def __call__(self, func): 40 | self._func = func 41 | 42 | from functools import wraps 43 | 44 | @wraps(func) 45 | def obj_call(*args, **kwargs): 46 | self.emit_signal(args, kwargs) 47 | return obj_call 48 | 49 | def emit_signal(self, args, kwargs): 50 | self.through_thread.emit(args, kwargs) 51 | 52 | def on_signal_received(self, args, kwargs): 53 | try: 54 | if self.inclass: 55 | obj, args = args[0], args[1:] 56 | self._func(obj, *args, **kwargs) 57 | else: 58 | self._func(*args, **kwargs) 59 | except Exception: 60 | import traceback 61 | traceback.print_exc() 62 | 63 | 64 | def touch(path): 65 | import os 66 | 67 | if not os.path.exists(path): 68 | basedir = os.path.dirname(path) 69 | 70 | if not os.path.exists(basedir): 71 | os.makedirs(basedir) 72 | 73 | with open(path, 'a'): 74 | os.utime(path) 75 | 76 | def get_free_port(): 77 | """ 78 | Determines a free port using sockets. 79 | """ 80 | import socket 81 | 82 | free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 83 | free_socket.bind(('0.0.0.0', 0)) 84 | free_socket.listen(5) 85 | port = free_socket.getsockname()[1] 86 | free_socket.close() 87 | 88 | return port 89 | 90 | def is_port_in_use(port): 91 | import socket 92 | 93 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 94 | return s.connect_ex(('127.0.0.1', port)) == 0 95 | 96 | def string_to_base64(text): 97 | import base64 98 | return str(base64.b64encode(str(text).encode("utf-8")), "utf-8") 99 | 100 | def get_local_ip(): 101 | try: 102 | import socket 103 | 104 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 105 | s.connect(("8.8.8.8", 80)) 106 | return s.getsockname()[0] 107 | except OSError: 108 | import sys 109 | print("Network is unreachable") 110 | sys.exit() 111 | 112 | def popen_and_call(popen_args, on_exit): 113 | """ 114 | Runs the given args in a subprocess.Popen, and then calls the function 115 | on_exit when the subprocess completes. 116 | on_exit is a callable object, and popen_args is a list/tuple of args that 117 | would give to subprocess.Popen. 118 | """ 119 | def run_in_thread(on_exit, popen_args): 120 | import subprocess 121 | 122 | try: 123 | proc = subprocess.Popen(popen_args, 124 | stdin=subprocess.PIPE, 125 | stdout=subprocess.PIPE, 126 | stderr=subprocess.STDOUT) 127 | proc.wait() 128 | except OSError: 129 | import traceback 130 | traceback.print_exc() 131 | on_exit() 132 | return 133 | import threading 134 | 135 | thread = threading.Thread(target=run_in_thread, args=(on_exit, popen_args)) 136 | thread.start() 137 | # returns immediately after the thread starts 138 | return thread 139 | 140 | def call_and_check_code(popen_args, on_exit): 141 | """ 142 | Runs the given args in a subprocess.Popen, and then calls the function 143 | on_exit when the subprocess completes. 144 | on_exit is a callable object, and popen_args is a list/tuple of args that 145 | would give to subprocess.Popen. 146 | """ 147 | def run_in_thread(on_exit, popen_args): 148 | import subprocess 149 | 150 | retcode = subprocess.call(popen_args, 151 | stdin=subprocess.PIPE, 152 | stdout=subprocess.PIPE, 153 | stderr=subprocess.STDOUT) 154 | on_exit(retcode) 155 | return 156 | 157 | import threading 158 | 159 | thread = threading.Thread(target=run_in_thread, args=(on_exit, popen_args)) 160 | thread.start() 161 | # returns immediately after the thread starts 162 | return thread 163 | 164 | def get_clipboard_text(): 165 | ''' Get text from system clipboard.''' 166 | from PyQt6.QtGui import QClipboard 167 | from PyQt6.QtWidgets import QApplication 168 | 169 | clipboard = QApplication.clipboard() 170 | text = clipboard.text() 171 | if text: 172 | return text 173 | 174 | if clipboard.supportsSelection(): 175 | return clipboard.text(QClipboard.Mode.Selection) 176 | 177 | return "" 178 | 179 | def set_clipboard_text(text): 180 | ''' Set text to system clipboard.''' 181 | from PyQt6.QtGui import QClipboard 182 | from PyQt6.QtWidgets import QApplication 183 | 184 | clipboard = QApplication.clipboard() 185 | clipboard.setText(text) 186 | 187 | if clipboard.supportsSelection(): 188 | clipboard.setText(text, QClipboard.Mode.Selection) 189 | 190 | 191 | def interactive(insert_or_do = False, msg_emacs = None, new_name = None): 192 | """ 193 | Defines an interactive command invoked from Emacs. 194 | """ 195 | def wrap(f, insert_or_do = insert_or_do, msg_emacs = msg_emacs, new_name = new_name): 196 | from functools import wraps 197 | 198 | f.interactive = True 199 | f.insert_or_do = insert_or_do 200 | f.msg_emacs = msg_emacs 201 | f.new_name = new_name 202 | 203 | @wraps(f) 204 | def wrapped_f(*args, **kwargs): 205 | return f(*args, **kwargs) 206 | return wrapped_f 207 | 208 | # Support both @interactive and @interactive() as valid syntax. 209 | if callable(insert_or_do): 210 | return wrap(insert_or_do, insert_or_do = False, msg_emacs = None, new_name = None) 211 | else: 212 | return wrap 213 | 214 | 215 | def abstract(f): 216 | """ 217 | Add a `abstract` flag to a method, 218 | 219 | We don't use abs.abstractmethod cause we don't need strict 220 | implementation check. 221 | """ 222 | from functools import wraps 223 | 224 | f.abstract = True 225 | @wraps(f) 226 | def wrap(*args, **kwargs): 227 | return f(*args, **kwargs) 228 | return wrap 229 | 230 | epc_client = None 231 | 232 | def init_epc_client(emacs_server_port): 233 | from epc.client import EPCClient 234 | 235 | global epc_client 236 | 237 | if epc_client is None: 238 | try: 239 | epc_client = EPCClient(("127.0.0.1", emacs_server_port), log_traceback=True) 240 | except ConnectionRefusedError: 241 | import traceback 242 | traceback.print_exc() 243 | 244 | def close_epc_client(): 245 | global epc_client 246 | 247 | if epc_client is not None: 248 | epc_client.close() 249 | 250 | 251 | def handle_arg_types(arg): 252 | if isinstance(arg, str) and arg.startswith("'"): 253 | arg = sexpdata.Symbol(arg.partition("'")[2]) 254 | 255 | return sexpdata.Quoted(arg) 256 | 257 | def eval_in_emacs(method_name, args): 258 | global epc_client 259 | 260 | args = [sexpdata.Symbol(method_name)] + list(map(handle_arg_types, args)) # type: ignore 261 | sexp = sexpdata.dumps(args) 262 | 263 | epc_client.call("eval-in-emacs", [sexp]) # type: ignore 264 | 265 | 266 | def get_emacs_func_result(method_name, args): 267 | global epc_client 268 | 269 | args = [sexpdata.Symbol(method_name)] + list(map(handle_arg_types, args)) # type: ignore 270 | sexp = sexpdata.dumps(args) 271 | 272 | result = epc_client.call_sync("get-emacs-func-result", [sexp]) # type: ignore 273 | return result if result != [] else False 274 | 275 | def get_app_dark_mode(app_dark_mode_var): 276 | app_dark_mode = get_emacs_var(app_dark_mode_var) 277 | return (app_dark_mode == "force" or \ 278 | app_dark_mode is True or \ 279 | (app_dark_mode == "follow" and \ 280 | get_emacs_theme_mode() == "dark")) 281 | 282 | def get_emacs_theme_mode(): 283 | return get_emacs_func_result("eaf-get-theme-mode", []) 284 | 285 | def get_emacs_theme_background(): 286 | return get_emacs_func_result("eaf-get-theme-background-color", []) 287 | 288 | def get_emacs_theme_foreground(): 289 | return get_emacs_func_result("eaf-get-theme-foreground-color", []) 290 | 291 | def message_to_emacs(message, prefix=True, logging=True): 292 | eval_in_emacs('eaf--show-message', [message, prefix, logging]) 293 | 294 | def clear_emacs_message(): 295 | eval_in_emacs('eaf--clear-message', []) 296 | 297 | def set_emacs_var(var_name, var_value): 298 | eval_in_emacs('eaf--set-emacs-var', [var_name, var_value]) 299 | 300 | def open_url_in_background_tab(url): 301 | eval_in_emacs('eaf-open-browser-in-background', [url]) 302 | 303 | def duplicate_page_in_new_tab(url): 304 | eval_in_emacs('eaf-browser--duplicate-page-in-new-tab', [url]) 305 | 306 | def open_url_in_new_tab(url): 307 | eval_in_emacs('eaf-open-browser', [url]) 308 | 309 | def open_url_in_new_tab_same_window(url, current_url): 310 | eval_in_emacs("eaf-open-browser-same-window", [url, current_url]) 311 | 312 | def open_url_in_new_tab_other_window(url): 313 | eval_in_emacs('eaf-open-browser-other-window', [url]) 314 | 315 | def translate_text(text): 316 | eval_in_emacs('eaf-translate-text', [text]) 317 | 318 | def input_message(buffer_id, message, callback_tag, input_type, input_content, completion_list): 319 | eval_in_emacs('eaf--input-message', [buffer_id, message, callback_tag, input_type, input_content, completion_list]) 320 | 321 | def focus_emacs_buffer(buffer_id): 322 | eval_in_emacs('eaf-focus-buffer', [buffer_id]) 323 | 324 | def atomic_edit(buffer_id, focus_text): 325 | eval_in_emacs('eaf--atomic-edit', [buffer_id, focus_text]) 326 | 327 | def convert_emacs_bool(symbol_value, symbol_is_boolean): 328 | if symbol_is_boolean == "t": 329 | return symbol_value is True 330 | else: 331 | return symbol_value 332 | 333 | def get_emacs_vars(args): 334 | global epc_client 335 | 336 | return list(map(lambda result: convert_emacs_bool(result[0], result[1]) if result != [] else False, epc_client.call_sync("get-emacs-vars", args))) # type: ignore 337 | 338 | def get_emacs_var(var_name): 339 | global epc_client 340 | 341 | (symbol_value, symbol_is_boolean) = epc_client.call_sync("get-emacs-var", [var_name]) # type: ignore 342 | 343 | return convert_emacs_bool(symbol_value, symbol_is_boolean) 344 | 345 | emacs_config_dir = "" 346 | 347 | def get_emacs_config_dir(): 348 | import os 349 | 350 | global emacs_config_dir 351 | 352 | if emacs_config_dir == "": 353 | emacs_config_dir = os.path.join(os.path.expanduser(get_emacs_var("eaf-config-location")), '') 354 | 355 | if not os.path.exists(emacs_config_dir): 356 | os.makedirs(emacs_config_dir) 357 | 358 | return emacs_config_dir 359 | 360 | def to_camel_case(string): 361 | components = string.split('_') 362 | return components[0] + ''.join(x.title() for x in components[1:]) 363 | 364 | emacs_func_cache_dict = {} 365 | 366 | def get_emacs_func_cache_result(func_name, func_args): 367 | global emacs_func_cache_dict 368 | 369 | if func_name in emacs_func_cache_dict: 370 | return emacs_func_cache_dict[func_name] 371 | else: 372 | result = get_emacs_func_result(func_name, func_args) 373 | emacs_func_cache_dict[func_name] = result 374 | 375 | return result 376 | 377 | current_desktop = os.getenv("XDG_CURRENT_DESKTOP") or os.getenv("XDG_SESSION_DESKTOP") 378 | 379 | def post_event(widget, event): 380 | try: 381 | QApplication.postEvent(widget, event) 382 | except: 383 | import traceback 384 | print("post_event error: " + traceback.format_exc()) 385 | 386 | def get_qrcode_pixmap(content): 387 | import tempfile 388 | 389 | import qrcode 390 | 391 | img = qrcode.make(content) 392 | temp_qrcode_file = tempfile.NamedTemporaryFile(mode="w", delete=False) 393 | temp_qrcode_file_path = temp_qrcode_file.name 394 | img.save(temp_qrcode_file_path) 395 | 396 | pixmap = QtGui.QPixmap(temp_qrcode_file_path) 397 | os.remove(temp_qrcode_file_path) 398 | 399 | return pixmap 400 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./README.zh-CN.md) 2 | 3 |

4 | 5 |
A free/libre and open-source extensible framework that revolutionizes the graphical capabilities of Emacs.
The key to ultimately Live in Emacs 6 |

7 | 8 | ## Vision 9 | 10 | 11 | Emacs, the extensible *text editor*, is more than 45 years old. It is older than virtually all operating systems people use today, almost as old as the first UNIX system. During the decades of development, the world's brightest hackers have contributed their intelligence and creativity. Together they've constructed the most comprehensive and richest ecosystem that you can find in any software to date. 12 | 13 | If you are a hacker who works with numerous languages and text, who's keyboard-driven and desires maximum freedom, extensibility, and introspectivity over your tool, maybe to the extent of *living* in it, Emacs will be your best bet. 14 | 15 | Unfortunately, this 45 years of age is also one of the greatest disadvantages of Emacs. Comparing with modern software, Emacs lacks performance. Specifically Emacs Lisp lacks performance. It doesn't have proper multithreading and its graphical capabilities are seriously limited. It is far from what you'd expect from any GUI application today (such as IDEA or VSCode). You may think that Emacs, as a text-centric editor, doesn't need them, but have you ever run into a situation that, you sit comfortably typing commands and doing your Emacs sorcery, but can't help but ponder: 16 | 17 | What if Emacs had a real browser? 18 | What if this PDF or video file could be viewed efficiently without leaving Emacs? 19 | 20 | Emacs, although infinitely extensible in text, is very limited in graphics. It shouldn't have to be this way. However, Emacs Lisp is *the* integral part of the Emacs culture, it carries decades of history with itself, it is what makes Emacs special. It is irreplaceable. 21 | 22 | The vision of the Emacs Application Framework (EAF) project is, while fully retaining the rich history, culture, and ecosystem of Emacs and Emacs Lisp, to open up completely new doors to the ecosystems of Python, Qt6, and even JavaScript. EAF extends Emacs to the world of modern graphics, but still preserving the extensibility and customizability of Emacs. It will be the key to ultimately *Live in Emacs*. 23 | 24 | ## Features 25 | 26 | EAF is very extensible. We ship a lot of applications, feel free to choose anything you find interesting to install: 27 | 28 | | Browser | PDF Viewer | 29 | | :--------: | :----: | 30 | | | | 31 | 32 | | Music Player | File Manager | 33 | | :--------: | :----: | 34 | | | | 35 | | | | 36 | 37 | - [Browser](https://github.com/emacs-eaf/eaf-browser): A modern, customizable and extensible browser in Emacs 38 | - [PDF Viewer](https://github.com/emacs-eaf/eaf-pdf-viewer): Fastest PDF Viewer in Emacs 39 | - [Music Player](https://github.com/emacs-eaf/eaf-music-player): Music player that supports playlist and audio visualization 40 | - [Video Player](https://github.com/emacs-eaf/eaf-video-player): Video Player in Emacs 41 | - [Image Viewer](https://github.com/emacs-eaf/eaf-image-viewer): Dynanmic image viewer 42 | - [RSS Reader](https://github.com/emacs-eaf/eaf-rss-reader): RSS Reader in Emacs 43 | - [Terminal](https://github.com/mumu-lhl/eaf-pyqterminal): Full-featured terminal in Emacs 44 | - [MindMap](https://github.com/emacs-eaf/eaf-mind-elixir): Mind map with balance layout 45 | - [Camera](https://github.com/emacs-eaf/eaf-camera): Use camera in Emacs 46 | - [Markdown Previewer](https://github.com/emacs-eaf/eaf-markdown-previewer): Real-time Markdown previewer 47 | - [Org Previewer](https://github.com/emacs-eaf/eaf-org-previewer): Real-time Org-mode previewer 48 | - [Git Client](https://github.com/emacs-eaf/eaf-git): Fully multi-threaded git client for Emacs 49 | - [File Manager](https://github.com/emacs-eaf/eaf-file-manager): Fully multi-threaded replacement for dired-mode 50 | - [Video Editor](https://github.com/ginqi7/eaf-video-editor): A simple video editor 51 | 52 | ... plus [many more](https://github.com/orgs/emacs-eaf/repositories)! 53 | 54 | ### EAF in EmacsConf 55 | | EmacsConf 2020: Extend Emacs with EAF | EmacsConf 2021: EAF: A 2021 Update | 56 | | :--------: | :----: | 57 | | [](https://www.youtube.com/watch?v=HK_f8KTuR0s) | [](https://www.youtube.com/watch?v=bh37zbefZk4) | 58 | | | | 59 | 60 | 61 | ## Install 62 | 63 | EAF supports Linux X11, Windows, macOS and FreeBSD. The installation method is very simple. 64 | 65 | If you use Nix、 macOS or Gentoo, you need check [Wiki](https://github.com/emacs-eaf/emacs-application-framework/wiki) first 66 | 67 | 68 | #### 1. Download EAF 69 | 70 | ```Bash 71 | git clone --depth=1 -b master https://github.com/emacs-eaf/emacs-application-framework.git ~/.emacs.d/site-lisp/emacs-application-framework/ 72 | ``` 73 | 74 | #### 2. Install/Update EAF applications and dependencies 75 | 76 | You can use `M-x eaf-install-and-update` or manually run the `install-eaf.py` script in the EAF directory: 77 | 78 | ```Bash 79 | cd emacs-application-framework 80 | chmod +x ./install-eaf.py 81 | ./install-eaf.py 82 | ``` 83 | 84 | There are many useful flags available for `install-eaf.py`, check it yourself using `--help`. 85 | 86 | #### 3. Load EAF Core 87 | 88 | From here on, you can add the full path to the EAF installation directory to your Emacs ```load-path```, then add the following to `init.el`: 89 | 90 | ```Elisp 91 | (add-to-list 'load-path "~/.emacs.d/site-lisp/emacs-application-framework/") 92 | (require 'eaf) 93 | ``` 94 | 95 | - Alternatively, if you use [use-package](https://github.com/jwiegley/use-package), you can use the following *sample* configuration for your convenience. 96 | 97 | ```Elisp 98 | (use-package eaf 99 | :load-path "~/.emacs.d/site-lisp/emacs-application-framework" 100 | :custom 101 | ; See https://github.com/emacs-eaf/emacs-application-framework/wiki/Customization 102 | (eaf-browser-continue-where-left-off t) 103 | (eaf-browser-enable-adblocker t) 104 | (browse-url-browser-function 'eaf-open-browser) 105 | :config 106 | (defalias 'browse-web #'eaf-open-browser) 107 | (eaf-bind-key scroll_up "C-n" eaf-pdf-viewer-keybinding) 108 | (eaf-bind-key scroll_down "C-p" eaf-pdf-viewer-keybinding) 109 | (eaf-bind-key take_photo "p" eaf-camera-keybinding) 110 | (eaf-bind-key nil "M-q" eaf-browser-keybinding)) ;; unbind, see more in the Wiki 111 | ``` 112 | 113 | #### 4. Load EAF Apps 114 | 115 | You can use below code to load applications `browser` and `pdf-viewer` that you installed. Please check [Applications](https://github.com/emacs-eaf/emacs-application-framework#features) for the full list: 116 | 117 | ```Elisp 118 | (require 'eaf-browser) 119 | (require 'eaf-pdf-viewer) 120 | ``` 121 | 122 | #### 5. Hooray! 123 | 124 | Congratulations, you just installed EAF! You can try `M-x eaf-open-demo` (that is if you have `demo` installed, of course) to see if everything works properly, and enjoy the new possibilities of Emacs. 125 | 126 | Below are launch commands of EAF Applications: 127 | 128 | | Application Name | Launch | 129 | | :-------- | :---- | 130 | | Browser | `M-x eaf-open-browser` Search or Goto URL | 131 | | | `M-x eaf-open-browser-with-history` Search or Goto URL or Goto History | 132 | | HTML Email Renderer | `M-x eaf-open-mail-as-html` in `gnus`, `mu4e`, `notmuch` HTMl Mail | 133 | | PDF Viewer | `M-x eaf-open` PDF File | 134 | | Video Player | `M-x eaf-open` Video File | 135 | | Image Viewer | `M-x eaf-open` Image File | 136 | | Markdown Previewer | `M-x eaf-open` Markdown File, select `markdown-previewer` | 137 | | Org Previewer | `M-x eaf-open` Org File, select `org-previewer` | 138 | | Camera | `M-x eaf-open-camera` | 139 | | Terminal | `M-x eaf-open-pyqterminal` | 140 | | File Manager | `M-x eaf-open-in-file-manager` | 141 | | RSS Reader | `M-x eaf-open-rss-reader` | 142 | | Git Client | `M-x eaf-open-git` | 143 | | Map Route Planning | `M-x eaf-open-map` | 144 | | File Sender | `M-x eaf-file-sender-qrcode` or `eaf-file-sender-qrcode-in-dired` | 145 | | File Browser | `M-x eaf-file-browser-qrcode` | 146 | | Airshare | `M-x eaf-open-airshare` | 147 | | Markmap | `M-x eaf-open` Markdown or Org file, select `markmap` | 148 | | Mindmap | `M-x eaf-create-mindmap` or `M-x eaf-open-mindmap` | 149 | | MS Office Viewer | `M-x eaf-open-office` | 150 | | Jupyter | `M-x eaf-open-jupyter` | 151 | | Music Player | `M-x eaf-open-music-player` | 152 | | System Monitor | `M-x eaf-open-system-monitor` | 153 | | Demo | `M-x eaf-open-demo` to verify basic functionality | 154 | | Vue Demo | `M-x eaf-open-vue-demo` to verify vue.js functionality | 155 | 156 | - EAF Browser and PDF Viewer support Emacs built-in bookmark operation, with `M-x bookmark-set` (defaulted to `C-x r m`) and `M-x bookmark-bmenu-list` (defaulted to `C-x r l`). 157 | 158 | ## Upgrade 159 | Also, you should regularly `git pull` **and** run `install-eaf.py` (`M-x eaf-install-and-update`) to update EAF, its applications, and relating dependencies. 160 | 161 | ## Report bug 162 | 163 | ### For any installation and configuration assistance, please read the [Wiki](https://github.com/emacs-eaf/emacs-application-framework/wiki) and [FAQ](https://github.com/emacs-eaf/emacs-application-framework/wiki/FAQ). 164 | 165 | If you encounter a problem with EAF, and it occurred after pulling the latest commit, please check the [Mandatory Procedures to Keep Your EAF Up-To-Date](https://github.com/emacs-eaf/emacs-application-framework/discussions/527?sort=new) page **first**. 166 | 167 | For any other problems, please use `emacs -q` and load a minimal setup with only EAF to verify that the bug is reproducible. If `emacs -q` works fine, probably something is wrong with your Emacs config. 168 | 169 | If the problem persists, please report it [here](https://github.com/emacs-eaf/emacs-application-framework/issues/new) with the `*eaf*` buffer content. It contains many clues that can help us locate the problem faster. 170 | 171 | If you get a segfault error, please use the following way to collect crash information: 172 | 1. Install gdb and turn on option `(setq eaf-enable-debug t)` 173 | 2. Use the command `eaf-stop-process` to stop the current process 174 | 3. Restart eaf, send issue with `*eaf*` buffer content when next crash 175 | 176 | ## EAF in the community 177 | 178 | A list of other community packages that use EAF to enhance their graphical experiences! 179 | 180 | If we missed your package, please make a PR to add it to the list. 181 | 182 | * ***[obr-viz](https://github.com/swhalemwo/obr-viz)***: visualizing [org-brain](https://github.com/Kungsgeten/org-brain) relationships using EAF 183 | * ***[netease-cloud-music](https://github.com/SpringHan/netease-cloud-music.el)***: A netease music client for emacs. 184 | * ***[2048pyqt6](https://github.com/porrige/2048pyqt6)***: A 2048 game that can run in emacs. 185 | * ***[pyqterminal](https://github.com/mumu-lhl/eaf-pyqterminal)***: A terminal written in PyQt6. 186 | 187 | ## Contributor 188 | 189 | 190 | 191 | 192 | ## Join Us 193 | Do you want to make Emacs a real "operating system"? 194 | 195 | Do you want to live in Emacs more comfortably? 196 | 197 | Do you want to revolutionize the capabilities of Emacs? 198 | 199 | [Let's hack together!](https://github.com/emacs-eaf/emacs-application-framework/wiki/Hacking) 200 | -------------------------------------------------------------------------------- /core/js/marker.js: -------------------------------------------------------------------------------- 1 | try { 2 | let Marker = {}; 3 | window.Marker = Marker; 4 | 5 | function getVisibleElements(filter) { 6 | let all = Array.from(document.documentElement.getElementsByTagName("*")); 7 | let visibleElements = []; 8 | for (let i = 0; i < all.length; i++) { 9 | let e = all[i]; 10 | // include elements in a shadowRoot. 11 | if (e.shadowRoot) { 12 | let cc = e.shadowRoot.querySelectorAll('*'); 13 | for (let j = 0; j < cc.length; j++) { 14 | all.push(cc[j]); 15 | } 16 | } 17 | let rect = e.getBoundingClientRect(); 18 | if ( (rect.top <= window.innerHeight) && (rect.bottom >= 0) 19 | && (rect.left <= window.innerWidth) && (rect.right >= 0) 20 | && rect.height > 0 21 | && getComputedStyle(e).visibility !== 'hidden' 22 | ) { 23 | filter(e, visibleElements); 24 | } 25 | } 26 | return visibleElements; 27 | } 28 | 29 | function moveCursorToEnd(el) { 30 | if (typeof el.selectionStart == "number") { 31 | el.selectionStart = el.selectionEnd = el.value.length; 32 | } else if (typeof el.createTextRange != "undefined") { 33 | el.focus(); 34 | let range = el.createTextRange(); 35 | range.collapse(false); 36 | range.select(); 37 | } 38 | } 39 | 40 | function cssSelector(el) { 41 | let path = [], parent; 42 | while (parent = el.parentNode) { 43 | path.unshift(`${el.tagName}:nth-child(${[].indexOf.call(parent.children, el)+1})`); 44 | el = parent; 45 | } 46 | return `${path.join(' > ')}`.toLowerCase(); 47 | } 48 | 49 | function isElementClickable(e) { 50 | let clickSelectors = "a, button, select, input, textarea, summary, *[onclick], *[contenteditable=true], *.jfk-button, *.goog-flat-menu-button, *[role=button], *[role=link], *[role=menuitem], *[role=option], *[role=switch], *[role=tab], *[role=checkbox], *[role=combobox], *[role=menuitemcheckbox], *[role=menuitemradio], *.collapsed, *.expanded, *.dropdown, *.est_unselected, *.tab, *.mod-action-wrap, *.menu-item, [id^=couplet3_], *.eaf-file-manager-file-name, *.eaf-file-manager-preview-file-name, *.eaf-music-player-item, *.eaf-rss-reader-feed-item, *.eaf-rss-reader-article-item, *.item"; 51 | 52 | return e.matches(clickSelectors) || getComputedStyle(e).cursor.substr(0, 4) === "url("; 53 | } 54 | 55 | function isEditable(element) { 56 | return element 57 | && !element.disabled && (element.localName === 'textarea' 58 | || element.localName === 'select' 59 | || element.isContentEditable 60 | || (element.localName === 'input' && /^(?!button|checkbox|file|hidden|image|radio|reset|submit)/i.test(element.type))); 61 | } 62 | 63 | function isElementDrawn(e, rect) { 64 | var min = isEditable(e) ? 1 : 4; 65 | rect = rect || e.getBoundingClientRect(); 66 | return rect.width >= min || rect.height >= min; 67 | } 68 | 69 | function getRealRect(elm) { 70 | if(!elm.getBoundingClientRect){ 71 | return getRealRect(elm.parentNode); 72 | }; 73 | if (elm.childElementCount === 0) { 74 | let r = elm.getClientRects(); 75 | if (r.length === 3) { 76 | // for a clipped A tag 77 | return r[1]; 78 | } else if (r.length === 2) { 79 | // for a wrapped A tag 80 | return r[0]; 81 | } else { 82 | return elm.getBoundingClientRect(); 83 | } 84 | } else if (elm.childElementCount === 1 && elm.firstElementChild.textContent) { 85 | let r = elm.firstElementChild.getBoundingClientRect(); 86 | if (r.width === 0 || r.height === 0) { 87 | r = elm.getBoundingClientRect(); 88 | } 89 | return r; 90 | } else { 91 | return elm.getBoundingClientRect(); 92 | } 93 | } 94 | 95 | function filterOverlapElements(elements) { 96 | // filter out tiny elements 97 | elements = elements.filter(function(e) { 98 | let be = getRealRect(e); 99 | if (e.disabled || e.readOnly || !isElementDrawn(e, be)) { 100 | return false; 101 | } else if (e.matches("input, textarea, select, form") || e.contentEditable === "true") { 102 | return true; 103 | } else { 104 | let topElement = document.elementFromPoint(be.left + be.width/2, be.top + be.height/2); 105 | return !topElement || (topElement.shadowRoot && topElement.childElementCount === 0) || topElement.isSameNode(e) || e.contains(topElement) || topElement.contains(e); 106 | } 107 | }); 108 | 109 | // if an element has href, all its children will be filtered out. 110 | var elementWithHref = null; 111 | elements = elements.filter(function(e) { 112 | var flag = true; 113 | if (e.href) { 114 | elementWithHref = e; 115 | } 116 | if (elementWithHref && elementWithHref !== e && elementWithHref.contains(e)) { 117 | flag = false; 118 | } 119 | return flag; 120 | }); 121 | 122 | return filterAncestors(elements); 123 | } 124 | 125 | function last(array) { 126 | return array[array.length - 1]; 127 | } 128 | 129 | function filterAncestors(elements) { 130 | if (elements.length === 0) { 131 | return elements; 132 | } 133 | 134 | // filter out element which has its children covered 135 | let result = [last(elements)]; 136 | for (let i = elements.length - 2; i >= 0; i--) { 137 | if (!elements[i].contains(last(result))) { 138 | result.push(elements[i]); 139 | } 140 | } 141 | 142 | // To restore original order of elements 143 | return result.reverse(); 144 | } 145 | 146 | function cAdd1(keyCounter, index, maxDigit){ 147 | if(keyCounter[index] + 1 == maxDigit){ 148 | keyCounter[index] = 0; 149 | cAdd1(keyCounter, index + 1, maxDigit); 150 | } else { 151 | keyCounter[index]++; 152 | } 153 | } 154 | 155 | function generateKeys(markerContainer) { 156 | let lettersString = "%{marker_letters}"; 157 | let letters = lettersString.split(""); 158 | let nodeNum = markerContainer.children.length; 159 | let keyLen = nodeNum == 1 ? 1 : Math.ceil(Math.log(nodeNum)/Math.log(letters.length)); 160 | let keyCounter = []; 161 | for(let i = 0; i < keyLen; i++) keyCounter[i] = 0; 162 | for(let l = 0; l < nodeNum; l++) { 163 | let keyStr = ''; 164 | for(let k = 0; k < keyLen; k++) { 165 | let mark = document.createElement('span'); 166 | mark.setAttribute('class', 'eaf-mark'); 167 | let key = letters[keyCounter[k]]; 168 | mark.textContent = key; 169 | markerContainer.children[l].appendChild(mark); 170 | keyStr += key; 171 | cAdd1(keyCounter, 0, letters.length); 172 | } 173 | markerContainer.children[l].id = keyStr; 174 | } 175 | } 176 | 177 | 178 | Marker.generateMarker = (selectors) => { 179 | let style = document.createElement('style'); 180 | let offsetX = "%{marker_offset_x}"; 181 | let offsetY = "%{marker_offset_y}"; 182 | document.head.appendChild(style); 183 | style.type = 'text/css'; 184 | style.setAttribute('class', 'eaf-style darkreader'); 185 | style.appendChild(document.createTextNode('\ 186 | .eaf-mark {\ 187 | background: none;\ 188 | border: none;\ 189 | bottom: auto;\ 190 | box-shadow: none;\ 191 | color: black !important;\ 192 | cursor: auto;\ 193 | display: inline;\ 194 | float: none;\ 195 | font-size: inherit;\ 196 | font-variant: normal;\ 197 | font-weight: bold;\ 198 | height: auto;\ 199 | left: auto;\ 200 | letter-spacing: 0;\ 201 | line-height: 100%;\ 202 | margin: 0;\ 203 | max-height: none;\ 204 | max-width: none;\ 205 | min-height: 0;\ 206 | min-width: 0;\ 207 | opacity: 1;\ 208 | padding: 0;\ 209 | position: static;\ 210 | right: auto;\ 211 | text-align: left;\ 212 | text-decoration: none;\ 213 | text-indent: 0;\ 214 | text-shadow: none;\ 215 | text-transform: none;\ 216 | top: auto;\ 217 | vertical-align: baseline;\ 218 | white-space: normal;\ 219 | width: auto;\ 220 | z-index: 2140000001;\ 221 | }')); 222 | style.appendChild(document.createTextNode('\ 223 | .eaf-marker {\ 224 | position: fixed;\ 225 | display: block;\ 226 | white-space: nowrap;\ 227 | overflow: hidden;\ 228 | font-size: %{marker_fontsize}px;\ 229 | background: linear-gradient(to bottom, #ffdd6e 0%, #deb050 100%);\ 230 | padding-left: 3px;\ 231 | padding-right: 3px;\ 232 | border: 1px solid #c38a22;\ 233 | border-radius: 3px;\ 234 | box-shadow: 0px 3px 7px 0px rgba(0, 0, 0, 0.3);\ 235 | z-index: 2140000001;\ 236 | }')); 237 | 238 | let body = document.querySelector('body'); 239 | let markerContainer = document.createElement('div'); 240 | markerContainer.setAttribute('class', 'eaf-marker-container'); 241 | body.insertAdjacentElement('afterend', markerContainer); 242 | for(let i = 0; i < selectors.length; i++) { 243 | if(selectors[i] != undefined){ 244 | if(!selectors[i].tagName){ 245 | selectors[i] = selectors[i].parentNode; 246 | } 247 | let marker = document.createElement('div'); 248 | let rect = selectors[i].getBoundingClientRect(); 249 | marker.setAttribute('class', 'eaf-marker'); 250 | marker.setAttribute('style', 'left: ' + (rect.x + parseInt(offsetX)) + 'px; top: ' + (rect.y + parseInt(offsetY)) + 'px;'); 251 | marker.setAttribute('pointed-link', cssSelector(selectors[i])); 252 | markerContainer.appendChild(marker); 253 | } 254 | } 255 | generateKeys(markerContainer); 256 | }; 257 | 258 | Marker.getMarkerSelector = (key) => { 259 | let markers = document.querySelectorAll('.eaf-marker'); 260 | let match; 261 | for(let i = 0; i < markers.length; i++) { 262 | if(markers[i].id === key.toUpperCase()) { 263 | match = markers[i]; 264 | break; 265 | } 266 | } 267 | if (match !== undefined) { 268 | return match.getAttribute('pointed-link'); 269 | } else { 270 | return undefined; 271 | } 272 | }; 273 | 274 | Marker.gotoMarker = (key, callback)=>{ 275 | selector = Marker.getMarkerSelector(key); 276 | if (selector != undefined && callback != undefined){ 277 | return callback(document.querySelector(selector)); 278 | } else { 279 | return ""; 280 | } 281 | }; 282 | 283 | Marker.getMarkerText = (key) => { 284 | selector = Marker.getMarkerSelector(key); 285 | if (selector != undefined){ 286 | return document.querySelector(selector).innerText; 287 | } else { 288 | return ""; 289 | } 290 | }; 291 | 292 | Marker.getMarkerClass = (key) => { 293 | selector = Marker.getMarkerSelector(key); 294 | if (selector != undefined){ 295 | return document.querySelector(selector).className; 296 | } else { 297 | return ""; 298 | } 299 | }; 300 | 301 | // this is callback function which call by core.webengine.py get_mark_link 302 | Marker.getMarkerAction = (node) => { 303 | action = ""; 304 | if(node == null){ 305 | return action; 306 | } 307 | if(node.nodeName.toLowerCase() === 'select'){ 308 | action = "eaf::[select]focus"; 309 | node.focus(); 310 | }else if(node.nodeName.toLowerCase() === 'input' || 311 | node.nodeName.toLowerCase() === 'textarea') { 312 | if((node.getAttribute('type') === 'submit') || 313 | (node.getAttribute('type') === 'checkbox')){ 314 | action = "eaf::[" + node.nodeName + "&" + node.getAttribute('type') + "]click"; 315 | node.click(); 316 | } else { 317 | action = "eaf::focus_click_movecursor_to_end"; 318 | node.focus(); // focus 319 | node.click(); // show blink cursor 320 | moveCursorToEnd(node); // move cursor to the end of line after focus. 321 | } 322 | } else if(node.href != undefined && node.href != '' && node.getAttribute('href') != '' && 323 | node.getAttribute('class') != 'toggle'){ 324 | if (node.href.includes('javascript:void') || node.getAttribute('href') == '#'){ 325 | action = "eaf::[href]click"; 326 | node.click(); 327 | } else { 328 | return node.href; 329 | } 330 | } else if(isElementClickable(node)){ // special href # button 331 | action = "eaf::click"; 332 | node.click(); 333 | } else if(node.nodeName.toLowerCase() === 'p'|| 334 | node.nodeName.toLowerCase() === 'span') { // select text section 335 | action = "eaf::select_p_span"; 336 | window.getSelection().selectAllChildren(node); 337 | } 338 | return action; 339 | }; 340 | 341 | Marker.generateClickMarkerList = () => { 342 | let elements = getVisibleElements(function(e, v) { 343 | if(isElementClickable(e)) v.push(e); 344 | }); 345 | elements = filterOverlapElements(elements); 346 | return elements; 347 | }; 348 | 349 | Marker.generateTextMarkerList = () => { 350 | let elements = getVisibleElements(function(e, v) { 351 | let aa = e.childNodes; 352 | for (let i = 0, len = aa.length; i < len; i++) { 353 | if (aa[i].nodeType == Node.TEXT_NODE && aa[i].data.length > 0) { 354 | v.push(e); 355 | break; 356 | } 357 | } 358 | }); 359 | 360 | elements = Array.prototype.concat.apply([], elements.map(function (e) { 361 | let aa = e.childNodes; 362 | let bb = []; 363 | for (let i = 0, len = aa.length; i < len; i++) { 364 | if (aa[i].nodeType == Node.TEXT_NODE && aa[i].data.trim().length > 1) { 365 | bb.push(aa[i]); 366 | } 367 | } 368 | return bb; 369 | })); 370 | 371 | return elements; 372 | }; 373 | 374 | 375 | Marker.cleanupLinks = () => { 376 | try { 377 | document.querySelector('.eaf-marker-container').remove(); 378 | document.querySelector('.eaf-style').remove(); 379 | } catch (err) {} 380 | }; 381 | 382 | } catch (e) {} 383 | -------------------------------------------------------------------------------- /extension/eaf-interleave.el: -------------------------------------------------------------------------------- 1 | ;;; eaf-interleave.el --- Interleaving text books on EAF -*- lexical-binding: t -*- 2 | 3 | ;; Author: Sebastian Christ 4 | ;; URL: https://github.com/rudolfochrist/interleave 5 | ;; Version: 1.4.20161123-610 6 | ;; Fork: luhuaei 7 | 8 | ;; This file is not part of GNU Emacs 9 | 10 | ;; This file is free software; you can redistribute it and/or modify 11 | ;; it under the terms of the GNU General Public License as published by 12 | ;; the Free Software Foundation; either version 3, or (at your option) 13 | ;; any later version. 14 | 15 | ;; This program is distributed in the hope that it will be useful, 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | ;; GNU General Public License for more details. 19 | 20 | ;; For a full copy of the GNU General Public License 21 | ;; see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; In the past, textbooks were sometimes published as 'interleaved' 26 | ;; editions. That meant, each page was followed by a blank page and 27 | ;; ambitious students/scholars had the ability to take their notes 28 | ;; directly in their copy of the textbook. Newton and Kant were 29 | ;; prominent representatives of this technique. 30 | 31 | ;; Nowadays textbooks (or lecture material) come in PDF format. Although almost 32 | ;; every PDF Reader has the ability to add some notes to the PDF itself, it is 33 | ;; not as powerful as it could be. 34 | 35 | ;; This is what this minor mode tries to accomplish. It presents your PDF side by 36 | ;; side to an [[http://orgmode.org][Org Mode]] buffer with your notes, narrowing 37 | ;; down to just those passages that are relevant to the particular page in the 38 | ;; document viewer. 39 | 40 | ;;; Usage: 41 | 42 | ;;; Code: 43 | 44 | (require 'org) 45 | (require 'org-element) 46 | 47 | (defcustom eaf-interleave-org-notes-dir-list '("~/org/interleave_notes" ".") 48 | "List of directories to look into when opening notes org from a pdf file. 49 | 50 | The notes file is assumed to have the exact 51 | same base name as the pdf file (just that the file extension is 52 | .org instead of .pdf). 53 | 54 | If the notes org file is not found, it is created in the 55 | directory returned on doing `car' of this list (first element of 56 | the list). 57 | 58 | The notes file is searched in order from the first list element 59 | till the last; the search is aborted once the file is found. 60 | 61 | If a list element is \".\" or begins with \"./\", that portion is 62 | replaced with the pdf directory name. e.g. \".\" is interpreted 63 | as \"/pdf/file/dir/\", \"./notes\" is interpreted as 64 | \"/pdf/file/dir/notes/\"." 65 | :type '(repeat directory) 66 | :group 'eaf) 67 | 68 | (defcustom eaf-interleave-split-direction 'vertical 69 | "Specify how to split the notes buffer." 70 | :type '(choice (const vertical) 71 | (const horizontal)) 72 | :group 'eaf) 73 | 74 | (defcustom eaf-interleave-split-lines nil 75 | "Specify the number of lines the PDF buffer should be increased or decreased. 76 | 77 | If nil both buffers are split equally. If the number is positive, 78 | the window is enlarged. If the number is negative, the window is 79 | shrunken. 80 | 81 | If `eaf-interleave-split-direction' is 'vertical then the number is 82 | taken as columns." 83 | :type '(choice integer 84 | (const nil)) 85 | :group 'eaf) 86 | 87 | (defcustom eaf-interleave-disable-narrowing nil 88 | "Disable narrowing in notes/org buffer." 89 | :type 'boolean 90 | :group 'eaf) 91 | 92 | ;; variables 93 | (defvar eaf-interleave-org-buffer nil 94 | "Org notes buffer name.") 95 | 96 | (defvar eaf-interleave--window-configuration nil 97 | "Variable to store the window configuration before interleave mode was enabled.") 98 | 99 | (defconst eaf-interleave--page-note-prop "interleave_page_note" 100 | "The page note property string.") 101 | 102 | (defconst eaf-interleave--url-prop "interleave_url" 103 | "The pdf property string.") 104 | 105 | ;; Minor mode for the org file buffer containing notes 106 | (defvar eaf-interleave-mode-map (make-sparse-keymap) 107 | "Keymap while command `eaf-interleave-mode' is active in the org file buffer.") 108 | 109 | ;;;###autoload 110 | (define-minor-mode eaf-interleave-mode 111 | "Interleaving your text books since 2015. 112 | 113 | In the past, textbooks were sometimes published as 'interleaved' editions. 114 | That meant, each page was followed by a blank page and the ambitious student/ 115 | scholar had the ability to take their notes directly in their copy of the 116 | textbook. Newton and Kant were prominent representatives of this technique. 117 | 118 | Nowadays textbooks (or lecture material) come in PDF format. Although almost 119 | every PDF Reader has the ability to add some notes to the PDF itself, it is 120 | not as powerful as it could be. 121 | 122 | This is what this minor mode tries to accomplish. It presents your PDF side by 123 | pppside to an [[http://orgmode.org][Org Mode]] buffer with your notes, narrowing 124 | down to just those passages that are relevant to the particular page in the 125 | document viewer. 126 | 127 | The split direction is determined by the customizable variable 128 | `eaf-interleave-split-direction'. When `eaf-interleave-mode' is invoked 129 | with a prefix argument the inverse split direction is used 130 | e.g. if `eaf-interleave-split-direction' is 'vertical the buffer is 131 | split horizontally." 132 | :keymap eaf-interleave-mode-map 133 | (if eaf-interleave-mode 134 | (setq eaf-interleave-org-buffer (current-buffer)) 135 | ;; Disable the corresponding minor mode in the PDF file too. 136 | (setq eaf-interleave-org-buffer nil))) 137 | 138 | (defvar eaf-interleave-app-mode-map (make-sparse-keymap) 139 | "Keymap while command `eaf-interleave-app-mode' is active.") 140 | 141 | ;;;###autoload 142 | (define-minor-mode eaf-interleave-app-mode 143 | "Interleave view for the EAF app." 144 | :keymap eaf-interleave-app-mode-map) 145 | 146 | ;;; functions 147 | ;; interactive 148 | (defun eaf-interleave-sync-current-note () 149 | "Sync EAF buffer on current note" 150 | (interactive) 151 | (let ((url (org-entry-get-with-inheritance eaf-interleave--url-prop))) 152 | (cond ((and (string-prefix-p "/" url) (string-suffix-p "pdf" url t)) 153 | (eaf-interleave-sync-pdf-page-current)) 154 | ((string-prefix-p "http" url) 155 | (eaf-interleave-sync-browser-url-current)))) 156 | ) 157 | 158 | (defun eaf-interleave-sync-pdf-page-current () 159 | "Open PDF page for currently visible notes." 160 | (interactive) 161 | (let* ((pdf-page (org-entry-get-with-inheritance eaf-interleave--page-note-prop)) 162 | (pdf-url (org-entry-get-with-inheritance eaf-interleave--url-prop)) 163 | (buffer (eaf-interleave--find-buffer pdf-url))) 164 | (if buffer 165 | (progn 166 | (eaf-interleave--display-buffer buffer) 167 | (when pdf-page 168 | (with-current-buffer buffer 169 | (eaf-interleave--pdf-viewer-goto-page pdf-url pdf-page)))) 170 | (eaf-interleave--select-split-function) 171 | (eaf-interleave--open-pdf pdf-url) 172 | ))) 173 | 174 | (defun eaf-interleave-sync-next-note () 175 | "Move to the next set of notes. 176 | This shows the next notes and synchronizes the PDF to the right page number." 177 | (interactive) 178 | (eaf-interleave--switch-to-org-buffer) 179 | (widen) 180 | (org-forward-heading-same-level 1) 181 | (eaf-interleave--narrow-to-subtree) 182 | (org-show-subtree) 183 | (org-cycle-hide-drawers t) 184 | (eaf-interleave-sync-current-note)) 185 | 186 | (defun eaf-interleave-add-note () 187 | "Add note for the EAF buffer. 188 | 189 | If there are already notes for this url, jump to the notes 190 | buffer." 191 | (interactive) 192 | (if (derived-mode-p 'eaf-mode) 193 | (cond ((equal eaf--buffer-app-name "pdf-viewer") 194 | (eaf-interleave--pdf-add-note)) 195 | ((equal eaf--buffer-app-name "browser") 196 | (eaf-interleave--browser-add-note))) 197 | )) 198 | 199 | (defun eaf-interleave-add-file-url () 200 | "Add a new url on note if the property is none, else modify current url." 201 | (interactive) 202 | (let ((url (read-file-name "Please specify path: " nil nil t))) 203 | (org-entry-put (point) eaf-interleave--url-prop url))) 204 | 205 | (defun eaf-interleave-sync-previous-note () 206 | "Move to the previous set of notes. 207 | This show the previous notes and synchronizes the PDF to the right page number." 208 | (interactive) 209 | (eaf-interleave--switch-to-org-buffer) 210 | (widen) 211 | (eaf-interleave--goto-parent-headline eaf-interleave--page-note-prop) 212 | (org-backward-heading-same-level 1) 213 | (eaf-interleave--narrow-to-subtree) 214 | (org-show-subtree) 215 | (org-cycle-hide-drawers t) 216 | (eaf-interleave-sync-current-note)) 217 | 218 | (defun eaf-interleave-open-notes-file () 219 | "Find current EAF url corresponding note files if it exists." 220 | (interactive) 221 | (if (derived-mode-p 'eaf-mode) 222 | (cond ((equal eaf--buffer-app-name "pdf-viewer") 223 | (eaf-interleave--open-notes-file-for-pdf)) 224 | ((equal eaf--buffer-app-name "browser") 225 | (eaf-interleave--open-notes-file-for-browser)))) 226 | ) 227 | 228 | (defun eaf-interleave-quit () 229 | "Quit interleave mode." 230 | (interactive) 231 | (with-current-buffer eaf-interleave-org-buffer 232 | (widen) 233 | (goto-char (point-min)) 234 | (when (eaf-interleave--headlines-available-p) 235 | (org-overview)) 236 | (eaf-interleave-mode 0))) 237 | 238 | ;;;###autoload 239 | (defun eaf-interleave--open-notes-file-for-pdf () 240 | "Open the notes org file for the current pdf file if it exists. 241 | Else create it. It is assumed that the notes org file will have 242 | the exact same base name as the pdf file (just that the notes 243 | file will have a .org extension instead of .pdf)." 244 | (let ((org-file (concat (file-name-base eaf--buffer-url) ".org"))) 245 | (eaf-interleave--open-notes-file-for-app org-file))) 246 | 247 | (defun eaf-interleave--open-notes-file-for-browser () 248 | "Find current open interleave-mode org file, if exists, else 249 | will create new org file with URL. It is assumed that the notes 250 | org file will have the exact sam base name as the url domain." 251 | (unless (buffer-live-p eaf-interleave-org-buffer) 252 | (let* ((domain (url-domain (url-generic-parse-url eaf--buffer-url))) 253 | (org-file (concat domain ".org"))) 254 | (eaf-interleave--open-notes-file-for-app org-file)))) 255 | 256 | (defun eaf-interleave--open-notes-file-for-app (org-file) 257 | "Open the notes org file for the current url if it exists. 258 | Else create it." 259 | (let ((org-file-path (eaf-interleave--find-match-org eaf-interleave-org-notes-dir-list eaf--buffer-url)) 260 | (buffer (eaf-interleave--find-buffer eaf--buffer-url))) 261 | ;; Create the notes org file if it does not exist 262 | (unless org-file-path 263 | (setq org-file-path (eaf-interleave--ensure-org-file-exist eaf-interleave-org-notes-dir-list org-file))) 264 | ;; Open the notes org file and enable `eaf-interleave-mode' 265 | (find-file org-file-path) 266 | (eaf-interleave-mode) 267 | (eaf-interleave--select-split-function) 268 | (switch-to-buffer buffer) 269 | )) 270 | 271 | (defun eaf-interleave--select-split-function () 272 | "Determine which split function to use. 273 | 274 | This returns either `split-window-below' or `split-window-right' 275 | based on a combination of `current-prefix-arg' and 276 | `eaf-interleave-split-direction'." 277 | (let () 278 | (delete-other-windows) 279 | (if (string= eaf-interleave-split-direction "vertical") 280 | (split-window-right) 281 | (split-window-below)) 282 | (when (integerp eaf-interleave-split-lines) 283 | (if (eql eaf-interleave-split-direction 'horizontal) 284 | (enlarge-window eaf-interleave-split-lines) 285 | (enlarge-window-horizontally eaf-interleave-split-lines))) 286 | )) 287 | 288 | (defun eaf-interleave--go-to-page-note (url page) 289 | "Look up the notes for the current pdf PAGE. 290 | 291 | Effectively resolves the headline with the interleave_page_note 292 | property set to PAGE and returns the point. 293 | 294 | If `eaf-interleave-disable-narrowing' is non-nil then the buffer gets 295 | re-centered to the page heading. 296 | 297 | It (possibly) narrows the subtree when found." 298 | (with-current-buffer eaf-interleave-org-buffer 299 | (let ((property-list (org-map-entries (lambda () 300 | (let ((url (org-entry-get-with-inheritance eaf-interleave--url-prop)) 301 | (page (org-entry-get-with-inheritance eaf-interleave--page-note-prop))) 302 | (cons url page))))) 303 | point) 304 | (catch 'find-property 305 | (dolist (property property-list) 306 | (when (and (string= (car property) url) 307 | (string= (cdr property) (number-to-string page))) 308 | (widen) 309 | (org-back-to-heading t) 310 | (eaf-interleave--narrow-to-subtree) 311 | (org-show-subtree) 312 | (org-cycle-hide-drawers t) 313 | (setq point (point)) 314 | (throw 'find-property nil)))) 315 | point))) 316 | 317 | (defun eaf-interleave--narrow-to-subtree (&optional force) 318 | "Narrow buffer to the current subtree. 319 | 320 | If `eaf-interleave-disable-narrowing' is non-nil this 321 | function does nothing. 322 | 323 | When FORCE is non-nil `eaf-interleave-disable-narrowing' is 324 | ignored." 325 | (when (and (not (org-before-first-heading-p)) 326 | (or (not eaf-interleave-disable-narrowing) 327 | force)) 328 | (org-narrow-to-subtree))) 329 | 330 | (defun eaf-interleave--switch-to-org-buffer (&optional insert-newline-maybe position) 331 | "Switch to the notes buffer. 332 | 333 | Inserts a newline into the notes buffer if INSERT-NEWLINE-MAYBE 334 | is non-nil. 335 | If POSITION is non-nil move point to it." 336 | (if (derived-mode-p 'eaf-mode) 337 | (switch-to-buffer-other-window eaf-interleave-org-buffer) 338 | (switch-to-buffer eaf-interleave-org-buffer)) 339 | (when (integerp position) 340 | (goto-char position)) 341 | (when insert-newline-maybe 342 | (save-restriction 343 | (when eaf-interleave-disable-narrowing 344 | (eaf-interleave--narrow-to-subtree t)) 345 | (goto-char (point-max))) 346 | ;; Expand again. Sometimes the new content is outside the narrowed 347 | ;; region. 348 | (org-show-subtree) 349 | (redisplay) 350 | ;; Insert a new line if not already on a new line 351 | (when (not (looking-back "^ *" (line-beginning-position))) 352 | (org-return)))) 353 | 354 | (defun eaf-interleave--insert-heading-respect-content () 355 | "Create a new heading in the notes buffer. 356 | 357 | Adjust the level of the new headline according to the 358 | PARENT-HEADLINE. 359 | 360 | Return the position of the newly inserted heading." 361 | (org-insert-heading-respect-content) 362 | (let* ((parent-level 0 ) 363 | (change-level (if (> (org-element-property :level (org-element-at-point)) 364 | (1+ parent-level)) 365 | #'org-promote 366 | #'org-demote))) 367 | (while (/= (org-element-property :level (org-element-at-point)) 368 | (1+ parent-level)) 369 | (funcall change-level))) 370 | (point)) 371 | 372 | (defun eaf-interleave--create-new-note (url &optional title page) 373 | "Create a new headline for current EAF url." 374 | (let (new-note-position) 375 | (with-current-buffer eaf-interleave-org-buffer 376 | (save-excursion 377 | (widen) 378 | (setq new-note-position (eaf-interleave--insert-heading-respect-content)) 379 | (org-set-property eaf-interleave--url-prop url) 380 | (when title 381 | (insert (format "Notes for %s" title))) 382 | (when page 383 | (org-set-property eaf-interleave--page-note-prop (number-to-string page))) 384 | (eaf-interleave--narrow-to-subtree) 385 | (org-cycle-hide-drawers t))) 386 | (eaf-interleave--switch-to-org-buffer t new-note-position))) 387 | 388 | (defun eaf-interleave-sync-browser-url-current () 389 | "Sync current note url for browser" 390 | (let* ((web-url (org-entry-get-with-inheritance eaf-interleave--url-prop)) 391 | (buffer (eaf-interleave--find-buffer web-url))) 392 | (if buffer 393 | (eaf-interleave--display-buffer buffer) 394 | (eaf-interleave--select-split-function) 395 | (eaf-interleave--open-web-url web-url)))) 396 | 397 | (defun eaf-interleave--display-buffer (buffer) 398 | "Use already used window display buffer" 399 | (eaf-interleave--narrow-to-subtree) 400 | (display-buffer-reuse-mode-window buffer '(("mode" . "eaf-interleave-app-mode"))) 401 | (eaf-interleave--ensure-buffer-window buffer)) 402 | 403 | (defun eaf-interleave--goto-parent-headline (property) 404 | "Traverse the tree until the parent headline. 405 | 406 | Consider a headline with property PROPERTY as parent headline." 407 | (catch 'done 408 | (if (and (eql (org-element-type (org-element-at-point)) 'headline) 409 | (org-entry-get (point) property)) 410 | (org-element-at-point) 411 | (condition-case nil 412 | (org-up-element) 413 | ('error 414 | (throw 'done nil))) 415 | (eaf-interleave--goto-parent-headline property)))) 416 | 417 | (defun eaf-interleave--pdf-add-note () 418 | "EAF pdf-viewer-mode add note" 419 | (let* ((page (eaf-interleave--pdf-viewer-current-page eaf--buffer-url)) 420 | (position (eaf-interleave--go-to-page-note eaf--buffer-url page))) 421 | (if position 422 | (eaf-interleave--switch-to-org-buffer t position) 423 | (eaf-interleave--create-new-note eaf--buffer-url eaf--buffer-app-name page))) 424 | ) 425 | 426 | (defun eaf-interleave--browser-add-note () 427 | "EAF browser add note" 428 | (eaf-interleave--create-new-note eaf--buffer-url eaf--buffer-app-name)) 429 | 430 | (defun eaf-interleave--headlines-available-p () 431 | "True if there are headings in the notes buffer." 432 | (save-excursion 433 | (re-search-forward "^\* .*" nil t))) 434 | 435 | ;; utils 436 | (defun eaf-interleave--open-pdf (pdf-file-name) 437 | "Use EAF PdfViewer open this pdf-file-name document." 438 | (eaf-open pdf-file-name) 439 | (add-hook 'eaf-pdf-viewer-hook 'eaf-interleave-app-mode)) 440 | 441 | (defun eaf-interleave--open-web-url (url) 442 | "Use EAF Browser open current note web address" 443 | (eaf-open-browser url) 444 | (add-hook 'eaf-browser-hook 'eaf-interleave-app-mode)) 445 | 446 | (defun eaf-interleave--find-buffer (url) 447 | "find EAF buffer base url" 448 | (let (current-buffer) 449 | (catch 'find-buffer 450 | (dolist (buffer (buffer-list)) 451 | (with-current-buffer buffer 452 | (when (and 453 | (derived-mode-p 'eaf-mode) 454 | (equal eaf--buffer-url url)) 455 | (setq current-buffer buffer) 456 | (throw 'find-buffer t))))) 457 | current-buffer)) 458 | 459 | (defun eaf-interleave--kill-buffer (url) 460 | "Kill the current converter process and buffer." 461 | (let ((buffer (eaf-interleave--find-buffer url))) 462 | (kill-buffer buffer))) 463 | 464 | (defun eaf-interleave--pdf-viewer-current-page (url) 465 | "get current page index." 466 | (let ((id (buffer-local-value 'eaf--buffer-id (eaf-interleave--find-buffer url)))) 467 | (string-to-number (eaf-call-sync "execute_function" id "current_page")))) 468 | 469 | (defun eaf-interleave--pdf-viewer-goto-page (url page) 470 | "goto page" 471 | (let ((id (buffer-local-value 'eaf--buffer-id (eaf-interleave--find-buffer url)))) 472 | (eaf-call-async "handle_input_response" id "jump_page" page))) 473 | 474 | (defun eaf-interleave--ensure-buffer-window (buffer) 475 | "If BUFFER don't display, will use other window display" 476 | (if (get-buffer-window buffer) 477 | nil 478 | (eaf-interleave--select-split-function) 479 | (switch-to-buffer buffer))) 480 | 481 | (defun eaf-interleave--parse-current-dir (dir url) 482 | "If dir is '.' or begins with './', replace the '.' or './' with the current url name" 483 | (replace-regexp-in-string 484 | "^\\(\\.$\\|\\./\\).*" 485 | (file-name-directory url) 486 | dir nil nil 1)) 487 | 488 | (defun eaf-interleave--find-match-org (dir-list url) 489 | "Find corresponding org file base url on dir list" 490 | (let ((org-file (concat (file-name-base url) ".org")) 491 | path) 492 | (catch 'break 493 | (dolist (dir dir-list) 494 | (setq dir (eaf-interleave--parse-current-dir dir url)) 495 | (setq path (locate-file org-file (list dir))) 496 | (when path 497 | ;; return the first match 498 | (throw 'break path)))) 499 | path)) 500 | 501 | (defun eaf-interleave--ensure-org-file-exist (dir-list file-name) 502 | "If the org file directory exist return this path, else created directory." 503 | (let ((default-dir (nth 0 dir-list))) 504 | (if dir-list 505 | (progn 506 | (unless (file-exists-p default-dir) 507 | (make-directory default-dir)) 508 | (expand-file-name file-name default-dir)) 509 | (read-file-name "Path for org file: " "~/")))) 510 | 511 | (provide 'eaf-interleave) 512 | ;;; interleave.el ends here 513 | -------------------------------------------------------------------------------- /core/buffer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (C) 2018 Andy Stewart 5 | # 6 | # Author: Andy Stewart 7 | # Maintainer: Andy Stewart 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 3 of the License, or 12 | # any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program. If not, see . 21 | 22 | import abc 23 | import string 24 | import time 25 | 26 | from core.utils import * 27 | from PyQt6.QtCore import QEvent, Qt, QThread, pyqtSignal 28 | from PyQt6.QtGui import QColor, QCursor, QFocusEvent, QKeyEvent 29 | from PyQt6.QtWidgets import QApplication, QGraphicsScene 30 | 31 | QT_KEY_DICT = {} 32 | 33 | # Build char event. 34 | for char in string.ascii_lowercase: 35 | upper_char = char.upper() 36 | QT_KEY_DICT[char] = eval("Qt.Key.Key_{}".format(upper_char)) 37 | QT_KEY_DICT[upper_char] = eval("Qt.Key.Key_{}".format(upper_char)) 38 | 39 | # Build number event. 40 | for number in range(0, 10): 41 | QT_KEY_DICT[str(number)] = eval("Qt.Key.Key_{}".format(number)) 42 | 43 | QT_KEY_DICT.update({ 44 | ''':''': Qt.Key.Key_Colon, 45 | ''';''': Qt.Key.Key_Semicolon, 46 | '''.''': Qt.Key.Key_Period, 47 | ''',''': Qt.Key.Key_Comma, 48 | '''+''': Qt.Key.Key_Plus, 49 | '''-''': Qt.Key.Key_Minus, 50 | '''=''': Qt.Key.Key_Equal, 51 | '''_''': Qt.Key.Key_Underscore, 52 | '''[''': Qt.Key.Key_BracketLeft, 53 | ''']''': Qt.Key.Key_BracketRight, 54 | '''(''': Qt.Key.Key_BraceLeft, 55 | ''')''': Qt.Key.Key_BraceRight, 56 | '''{''': Qt.Key.Key_ParenLeft, 57 | '''}''': Qt.Key.Key_ParenRight, 58 | '''<''': Qt.Key.Key_Less, 59 | '''>''': Qt.Key.Key_Greater, 60 | '''@''': Qt.Key.Key_At, 61 | '''\\''': Qt.Key.Key_Backslash, 62 | '''|''': Qt.Key.Key_Bar, 63 | '''/''': Qt.Key.Key_Slash, 64 | '''#''': Qt.Key.Key_NumberSign, 65 | '''$''': Qt.Key.Key_Dollar, 66 | '''?''': Qt.Key.Key_Question, 67 | '''"''': Qt.Key.Key_QuoteDbl, 68 | '''`''': Qt.Key.Key_QuoteLeft, 69 | '''%''': Qt.Key.Key_Percent, 70 | '''^''': Qt.Key.Key_AsciiCircum, 71 | '''&''': Qt.Key.Key_Ampersand, 72 | '''*''': Qt.Key.Key_Asterisk, 73 | '''~''': Qt.Key.Key_AsciiTilde, 74 | '''!''': Qt.Key.Key_Exclam, 75 | '''\'''': Qt.Key.Key_Apostrophe, 76 | '''SPC''': Qt.Key.Key_Space, 77 | '''RET''': Qt.Key.Key_Return, 78 | '''DEL''': Qt.Key.Key_Backspace, 79 | '''TAB''': Qt.Key.Key_Tab, 80 | '''''': Qt.Key.Key_Backtab, 81 | '''''': Qt.Key.Key_Home, 82 | '''''': Qt.Key.Key_End, 83 | '''''': Qt.Key.Key_Left, 84 | '''''': Qt.Key.Key_Right, 85 | '''''': Qt.Key.Key_Up, 86 | '''''': Qt.Key.Key_Down, 87 | '''''': Qt.Key.Key_PageUp, 88 | '''''': Qt.Key.Key_PageDown, 89 | '''''': Qt.Key.Key_Delete, 90 | '''''': Qt.Key.Key_Backspace, 91 | '''''': Qt.Key.Key_Return, 92 | '''''': Qt.Key.Key_Escape 93 | }) 94 | 95 | # NOTE: 96 | # We need convert return or backspace to correct text, 97 | # otherwise EAF browser will crash when user type return/backspace key. 98 | QT_TEXT_DICT = { 99 | "SPC": " ", 100 | "": "RET", 101 | "": "", 102 | "": "", 103 | "": "", 104 | "": "", 105 | "": "", 106 | "": "", 107 | "": "", 108 | "": "", 109 | "": "", 110 | "": "", 111 | "": "", 112 | "": "" 113 | } 114 | 115 | QT_MODIFIER_DICT = { 116 | "C": Qt.KeyboardModifier.ControlModifier, 117 | "M": Qt.KeyboardModifier.AltModifier, 118 | "S": Qt.KeyboardModifier.ShiftModifier, 119 | "s": Qt.KeyboardModifier.MetaModifier 120 | } 121 | 122 | class Buffer(QGraphicsScene): 123 | __metaclass__ = abc.ABCMeta 124 | 125 | aspect_ratio_change = pyqtSignal() 126 | enter_fullscreen_request = pyqtSignal() 127 | exit_fullscreen_request = pyqtSignal() 128 | 129 | def __init__(self, buffer_id, url, arguments, fit_to_view): 130 | super(QGraphicsScene, self).__init__() 131 | 132 | self.buffer_id = buffer_id 133 | self.url = url 134 | self.arguments = arguments 135 | self.fit_to_view = fit_to_view 136 | self.title = "" 137 | self.current_event_string = "" 138 | 139 | self.buffer_widget = None 140 | self.is_fullscreen = False 141 | 142 | self.aspect_ratio = 0 143 | self.vertical_padding_ratio = 1.0 / 8 144 | 145 | self.fetch_marker_input_thread = None 146 | self.fetch_search_input_thread = None 147 | 148 | self.theme_mode = get_emacs_theme_mode() 149 | self.theme_foreground_color = get_emacs_theme_foreground() 150 | self.theme_background_color = get_emacs_theme_background() 151 | 152 | self.enter_fullscreen_request.connect(self.enable_fullscreen) 153 | self.exit_fullscreen_request.connect(self.disable_fullscreen) 154 | 155 | def base_class_name(self): 156 | return self.__class__.__bases__[0].__name__ 157 | 158 | def build_all_methods(self, origin_class): 159 | ''' Build all methods.''' 160 | method_list = [func for func in dir(origin_class) if callable(getattr(origin_class, func)) and not func.startswith("__")] 161 | for func_name in method_list: 162 | func_attr = getattr(origin_class, func_name) 163 | if hasattr(func_attr, "interactive"): 164 | self.build_interactive_method( 165 | origin_class, 166 | func_name, 167 | getattr(func_attr, "new_name"), 168 | getattr(func_attr, "insert_or_do")) 169 | 170 | def build_interactive_method(self, origin_class, class_method_name, new_method_name=None, insert_or_do=False): 171 | ''' Build interactive methods.''' 172 | new_name = class_method_name if new_method_name is None else new_method_name 173 | if (not hasattr(self, class_method_name)) or hasattr(getattr(self, class_method_name), "abstract"): 174 | self.__dict__.update({new_name: getattr(origin_class, class_method_name)}) 175 | if insert_or_do: 176 | self.build_insert_or_do(new_name) 177 | 178 | def build_insert_or_do(self, method_name): 179 | ''' Build insert or do.''' 180 | def _do (): 181 | if self.is_focus(): # type: ignore 182 | self.send_key(self.current_event_string) 183 | else: 184 | getattr(self, method_name)() 185 | 186 | setattr(self, "insert_or_{}".format(method_name), _do) 187 | 188 | def toggle_fullscreen(self): 189 | ''' Toggle full screen.''' 190 | if self.is_fullscreen: 191 | self.exit_fullscreen_request.emit() 192 | else: 193 | self.enter_fullscreen_request.emit() 194 | 195 | def enable_fullscreen(self): 196 | ''' Enable full screen.''' 197 | self.is_fullscreen = True 198 | eval_in_emacs('eaf--enter-fullscreen-request', []) 199 | 200 | def disable_fullscreen(self): 201 | ''' Disable full screen.''' 202 | self.is_fullscreen = False 203 | eval_in_emacs('eaf--exit_fullscreen_request', []) 204 | 205 | def move_cursor_to_corner(self): 206 | ''' 207 | Move cursor to bottom right corner of screen. 208 | ''' 209 | screen = QApplication.instance().primaryScreen() # type: ignore 210 | try: 211 | QCursor().setPos(screen, screen.size().width() - 1, screen.size().height() - 1) 212 | except: 213 | # Moves the cursor the primary screen to the global screen position (x, y). 214 | # Sometimes, setPos(QScreen, Int, Int) API don't exists. 215 | QCursor().setPos(screen.size().width() - 1, screen.size().height() - 1) 216 | 217 | def move_cursor_to_nearest_border(self): 218 | ''' 219 | Move cursor to nearest border of current buffer. 220 | ''' 221 | # get current cursor position 222 | cursor_pos = QCursor().pos() 223 | # get current screen 224 | screen = QApplication.instance().primaryScreen() # type: ignore 225 | 226 | c_x, c_y = cursor_pos.x(), cursor_pos.y() 227 | sc_width, sc_height = screen.size().width(), screen.size().height() 228 | left_dist, right_dist = c_x, sc_width - c_x 229 | top_dist, bottom_dist = c_y, sc_height - c_y 230 | min_dist = min(left_dist, right_dist, top_dist, bottom_dist) 231 | 232 | # move cursor to nearest border 233 | if min_dist == left_dist: 234 | QCursor().setPos(0, c_y) 235 | elif min_dist == right_dist: 236 | QCursor().setPos(sc_width-1, c_y) 237 | elif min_dist == top_dist: 238 | QCursor().setPos(c_x, 0) 239 | elif min_dist == bottom_dist: 240 | QCursor().setPos(c_x, sc_height-1) 241 | 242 | def set_aspect_ratio(self, aspect_ratio): 243 | ''' Set aspect ratio.''' 244 | self.aspect_ratio = aspect_ratio 245 | self.aspect_ratio_change.emit() 246 | 247 | def add_widget(self, widget): 248 | ''' Add widget.''' 249 | # Init background color before addWidget. 250 | if not hasattr(self, "background_color"): 251 | self.background_color = QColor(self.theme_background_color) 252 | 253 | self.buffer_widget = widget 254 | self.addWidget(self.buffer_widget) 255 | 256 | self.buffer_widget.installEventFilter(self) 257 | 258 | self.buffer_widget.buffer = self 259 | 260 | def destroy_buffer(self): 261 | ''' Destroy buffer.''' 262 | if self.buffer_widget is not None: 263 | self.buffer_widget.deleteLater() 264 | 265 | def change_title(self, new_title): 266 | ''' Change title.''' 267 | if new_title != "about:blank": 268 | self.title = new_title 269 | eval_in_emacs('eaf--update-buffer-details', [self.buffer_id, new_title, self.url]) 270 | 271 | @interactive(insert_or_do=True) 272 | def close_buffer(self): 273 | ''' Close buffer.''' 274 | eval_in_emacs('eaf-request-kill-buffer', [self.buffer_id]) 275 | 276 | @abstract 277 | def all_views_hide(self): 278 | pass 279 | 280 | @abstract 281 | def some_view_show(self): 282 | pass 283 | 284 | @abstract 285 | def resize_view(self): 286 | (_, _, width, height) = get_emacs_func_result("eaf-get-window-size-by-buffer-id", [self.buffer_id]) 287 | self.buffer_widget.resize(width, height) 288 | 289 | def get_key_event_widgets(self): 290 | ''' Get key event widgets.''' 291 | return [self.buffer_widget] 292 | 293 | def send_input_message(self, message, callback_tag, input_type="string", initial_content="", completion_list=[]): 294 | ''' Send an input message to Emacs side for the user to respond. 295 | 296 | MESSAGE is a message string that would be sent to the user. 297 | 298 | CALLBACK_TAG is the reference tag when handle_input_message is invoked. 299 | 300 | INPUT_TYPE must be one of "string", "file", "yes-or-no", "marker" or "search". 301 | 302 | INITIAL_CONTENT is the intial content of the user response, it is only useful when INPUT_TYPE is "string". 303 | ''' 304 | input_message(self.buffer_id, message, callback_tag, input_type, initial_content, completion_list) 305 | 306 | if input_type == "marker" and (not hasattr(getattr(self, "fetch_marker_callback"), "abstract")): 307 | self.start_marker_input_monitor_thread(callback_tag) 308 | elif input_type == "search": 309 | self.start_search_input_monitor_thread(callback_tag) 310 | 311 | @abstract 312 | def fetch_marker_callback(self): 313 | pass 314 | 315 | def start_marker_input_monitor_thread(self, callback_tag): 316 | self.fetch_marker_input_thread = FetchMarkerInputThread(callback_tag, self.fetch_marker_callback()) 317 | self.fetch_marker_input_thread.match_marker.connect(self.handle_input_response) 318 | self.fetch_marker_input_thread.start() 319 | 320 | def stop_marker_input_monitor_thread(self): 321 | if self.fetch_marker_input_thread is not None and self.fetch_marker_input_thread.isRunning(): 322 | self.fetch_marker_input_thread.stop() 323 | # NOTE: 324 | # We need call QThread.wait() function before reset QThread object to None. 325 | # Set to None will trigger Python GC release QThread object, if QThread is still running, 326 | # Segment falut "QThread: Destroyed while thread is still running" will throw, it will crash EAF. 327 | self.fetch_marker_input_thread.wait() 328 | self.fetch_marker_input_thread = None 329 | 330 | def start_search_input_monitor_thread(self, callback_tag): 331 | self.fetch_search_input_thread = FetchSearchInputThread(callback_tag) 332 | self.fetch_search_input_thread.search_changed.connect(self.handle_input_response) 333 | self.fetch_search_input_thread.search_finish.connect(self.handle_search_finish) 334 | self.fetch_search_input_thread.start() 335 | 336 | def stop_search_input_monitor_thread(self): 337 | if self.fetch_search_input_thread is not None and self.fetch_search_input_thread.isRunning(): 338 | self.fetch_search_input_thread.stop() 339 | # NOTE: 340 | # We need call QThread.wait() function before reset QThread object to None. 341 | # Set to None will trigger Python GC release QThread object, if QThread is still running, 342 | # Segment falut "QThread: Destroyed while thread is still running" will throw, it will crash EAF. 343 | self.fetch_search_input_thread.wait() 344 | self.fetch_search_input_thread = None 345 | 346 | @abstract 347 | def handle_input_response(self, callback_tag, result_content): 348 | pass 349 | 350 | @abstract 351 | def action_quit(self): 352 | pass 353 | 354 | @abstract 355 | def cancel_input_response(self, callback_tag): 356 | pass 357 | 358 | @abstract 359 | def handle_search_forward(self, callback_tag): 360 | pass 361 | 362 | @abstract 363 | def handle_search_backward(self, callback_tag): 364 | pass 365 | 366 | @abstract 367 | def handle_search_finish(self, callback_tag): 368 | pass 369 | 370 | @abstract 371 | def scroll_other_buffer(self, scroll_direction, scroll_type): 372 | pass 373 | 374 | def save_session_data(self): 375 | return "" 376 | 377 | @abstract 378 | def restore_session_data(self, session_data): 379 | pass 380 | 381 | @abstract 382 | def update_with_data(self, update_data): 383 | pass 384 | 385 | @abstract 386 | def eval_js_function(self, function_name, function_arguments): 387 | ''' Eval JavaScript function.''' 388 | pass 389 | 390 | @abstract 391 | def execute_js_function(self, function_name, function_arguments): 392 | ''' Execute JavaScript function and return result.''' 393 | return None 394 | 395 | @abstract 396 | def eval_js_code(self, function_name, function_arguments): 397 | ''' Eval JavaScript function.''' 398 | pass 399 | 400 | @abstract 401 | def execute_js_code(self, function_name, function_arguments): 402 | ''' Execute JavaScript function and return result.''' 403 | return None 404 | 405 | def execute_function(self, function_name): 406 | ''' Call function.''' 407 | return getattr(self, function_name)() 408 | 409 | def execute_function_with_args(self, function_name, *args, **kwargs): 410 | ''' Call function with arguments.''' 411 | return getattr(self, function_name)(*args, **kwargs) 412 | 413 | @abstract 414 | def send_key_filter(self, event_string): 415 | pass 416 | 417 | @PostGui() 418 | def send_key(self, event_string): 419 | ''' Fake key event.''' 420 | # Init. 421 | text = QT_TEXT_DICT.get(event_string, event_string) 422 | modifier = Qt.KeyboardModifier.NoModifier 423 | 424 | if event_string == "" or (len(event_string) == 1 and event_string.isupper()): 425 | modifier = Qt.KeyboardModifier.ShiftModifier 426 | 427 | # NOTE: don't ignore text argument, otherwise QWebEngineView not respond key event. 428 | try: 429 | key_press = QKeyEvent(QEvent.Type.KeyPress, QT_KEY_DICT[event_string], modifier, text) 430 | except: 431 | key_press = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_unknown, modifier, text) 432 | 433 | for widget in self.get_key_event_widgets(): 434 | post_event(widget, key_press) 435 | 436 | self.send_key_filter(event_string) 437 | 438 | @PostGui() 439 | def send_key_sequence(self, event_string): 440 | ''' Fake key sequence.''' 441 | event_list = event_string.split("-") 442 | 443 | if len(event_list) > 1: 444 | widget = self.buffer_widget.focusProxy() 445 | last_char = event_list[-1] 446 | last_key = last_char.lower() if len(last_char) == 1 else last_char 447 | 448 | modifier_keys = [QT_MODIFIER_DICT.get(modifier) for modifier in event_list[0:-1]] 449 | modifier_flags = Qt.KeyboardModifier.NoModifier 450 | for modifier in modifier_keys: 451 | modifier_flags |= modifier 452 | 453 | text = QT_TEXT_DICT.get(last_key, last_key) 454 | 455 | key_event = QKeyEvent(QEvent.Type.KeyPress, QT_KEY_DICT[last_key], modifier_flags, text) 456 | post_event(widget, key_event) 457 | 458 | def get_url(self): 459 | ''' Get url.''' 460 | return self.url 461 | 462 | def get_clipboard_text(self): 463 | ''' Get text from system clipboard.''' 464 | return get_clipboard_text() 465 | 466 | def set_clipboard_text(self, text): 467 | ''' Set text to system clipboard.''' 468 | set_clipboard_text(text) 469 | 470 | @interactive(insert_or_do=True) 471 | def save_as_bookmark(self): 472 | ''' Save as bookmark.''' 473 | eval_in_emacs('bookmark-set', []) 474 | 475 | @interactive(insert_or_do=True) 476 | def select_left_tab(self): 477 | ''' Select left tab.''' 478 | eval_in_emacs('eaf-goto-left-tab', []) 479 | 480 | @interactive(insert_or_do=True) 481 | def select_right_tab(self): 482 | ''' Select right tab.''' 483 | eval_in_emacs('eaf-goto-right-tab', []) 484 | 485 | @interactive 486 | def update_theme(self): 487 | self.theme_mode = get_emacs_theme_mode() 488 | self.theme_foreground_color = get_emacs_theme_foreground() 489 | self.theme_background_color = get_emacs_theme_background() 490 | 491 | def focus_widget(self, event=None): 492 | '''Focus buffer widget.''' 493 | if event is None: 494 | event = QFocusEvent(QEvent.Type.FocusIn, Qt.FocusReason.MouseFocusReason) 495 | 496 | post_event(self.buffer_widget.focusProxy(), event) # type: ignore 497 | 498 | # Activate emacs window when call focus widget, avoid first char is not 499 | eval_in_emacs('eaf-activate-emacs-window', []) 500 | 501 | class FetchMarkerInputThread(QThread): 502 | 503 | match_marker = pyqtSignal(str, str) 504 | 505 | def __init__(self, callback_tag, markers): 506 | QThread.__init__(self) 507 | 508 | self.callback_tag = callback_tag 509 | self.running_flag = True 510 | 511 | self.marker_quit_keys = get_emacs_var("eaf-marker-quit-keys") or "" 512 | self.markers = markers 513 | 514 | def run(self): 515 | while self.running_flag: 516 | if self.markers: 517 | minibuffer_input = get_emacs_func_result("minibuffer-contents-no-properties", []) 518 | 519 | marker_input_quit = minibuffer_input and len(minibuffer_input) > 0 and minibuffer_input[-1] in self.marker_quit_keys 520 | marker_input_finish = minibuffer_input in self.markers 521 | 522 | if marker_input_quit or marker_input_finish: 523 | self.running_flag = False 524 | eval_in_emacs('exit-minibuffer', []) 525 | message_to_emacs("Quit marker selection." if marker_input_quit else "Marker selected.") 526 | 527 | time.sleep(0.1) 528 | 529 | def stop(self): 530 | self.running_flag = False 531 | 532 | class FetchSearchInputThread(QThread): 533 | 534 | search_changed = pyqtSignal(str, str) 535 | search_finish = pyqtSignal(str) 536 | 537 | def __init__(self, callback_tag): 538 | QThread.__init__(self) 539 | 540 | self.search_string = "" 541 | self.callback_tag = callback_tag 542 | self.running_flag = True 543 | 544 | def run(self): 545 | while self.running_flag: 546 | in_minibuffer = get_emacs_func_result("minibufferp", []) 547 | 548 | if in_minibuffer: 549 | minibuffer_input = get_emacs_func_result("minibuffer-contents-no-properties", []) 550 | 551 | if minibuffer_input != self.search_string: 552 | self.search_changed.emit(self.callback_tag, minibuffer_input) 553 | self.search_string = minibuffer_input 554 | else: 555 | self.stop() 556 | 557 | time.sleep(0.1) 558 | 559 | def stop(self): 560 | self.search_finish.emit(self.callback_tag) 561 | self.running_flag = False 562 | -------------------------------------------------------------------------------- /install-eaf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import datetime 5 | import json 6 | import os 7 | import subprocess 8 | import sys 9 | import sysconfig 10 | from shutil import rmtree, which 11 | 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument("--install-all-apps", action="store_true", 14 | help='install/update all available applications') 15 | parser.add_argument("--install-core-deps", action="store_true", 16 | help='only install/update core dependencies') 17 | parser.add_argument("-i", "--install", nargs='+', default=[], 18 | help='only install/update apps listed here') 19 | parser.add_argument("--install-new-apps", action="store_true", 20 | help='also install previously uninstalled or new applications') 21 | parser.add_argument("-f", "--force", action="store_true", 22 | help="force install/update app dependencies even if apps are already up-to-date") 23 | parser.add_argument("--ignore-core-deps", action="store_true", 24 | help='ignore core dependencies') 25 | parser.add_argument("--ignore-sys-deps", action="store_true", 26 | help='ignore system dependencies') 27 | parser.add_argument("--ignore-py-deps", action="store_true", 28 | help='ignore python dependencies') 29 | parser.add_argument("--ignore-node-deps", action="store_true", 30 | help='ignore node dependencies') 31 | parser.add_argument("--git-full-clone", action="store_true", 32 | help='during installation, conduct a full clone to preserve git logs') 33 | parser.add_argument("--app-drop-local-edit", action="store_true", 34 | help='during installation, local changes to app repos will be hard reset') 35 | parser.add_argument("--app-save-local-edit", action="store_true", 36 | help='compared with --app-drop-local-edit, this option will stash your changes') 37 | args = parser.parse_args() 38 | 39 | NPM_CMD = "npm.cmd" if sys.platform == "win32" else "npm" 40 | PIP_CMD = "pip3" if which("pip3") else "pip" # mac only have pip3, so we need use pip3 instead pip 41 | 42 | class bcolors: 43 | HEADER = '\033[95m' 44 | OKBLUE = '\033[94m' 45 | OKCYAN = '\033[96m' 46 | OKGREEN = '\033[92m' 47 | WARNING = '\033[93m' 48 | FAIL = '\033[91m' 49 | ENDC = '\033[0m' 50 | BOLD = '\033[1m' 51 | UNDERLINE = '\033[4m' 52 | 53 | script_path = os.path.dirname(os.path.realpath(__file__)) 54 | def get_available_apps_dict(): 55 | with open(os.path.join(script_path, 'applications.json')) as f: 56 | info = json.load(f) 57 | apps_dict = {} 58 | for app_name, app_spec_dict in info.items(): 59 | if app_spec_dict["type"] == "app": 60 | apps_dict[app_name] = app_spec_dict 61 | return apps_dict 62 | 63 | available_apps_dict = get_available_apps_dict() 64 | 65 | install_failed_sys = [] 66 | install_failed_pys = [] 67 | install_failed_npm_globals = [] 68 | install_failed_apps = [] 69 | 70 | important_messages = [ 71 | "[EAF] Please run 'git pull && ./install-eaf.py' (M-x eaf-install-and-update) to update EAF and their dependencies.", 72 | "[EAF] Refer to dependencies.json for the list of dependencies installed on your system." 73 | ] 74 | 75 | def run_command(command, path=script_path, ensure_pass=True, get_result=False): 76 | print("[EAF] Running", ' '.join(command), "@", path) 77 | 78 | # Throw exception if command not found, 79 | # We found install-eaf.py still can work even it not found npm in system. 80 | if not which(command[0]): 81 | return Exception(f"Not found command: {command[0]}") 82 | 83 | # Use LC_ALL=C to make sure command output use English. 84 | # Then we can use English keyword to check command output. 85 | english_env = os.environ.copy() 86 | english_env['LC_ALL'] = 'C' 87 | 88 | if get_result: 89 | process = subprocess.Popen(command, env = english_env, stdin = subprocess.PIPE, 90 | universal_newlines=True, text=True, cwd=path, 91 | stdout = subprocess.PIPE) 92 | else: 93 | process = subprocess.Popen(command, env = english_env, stdin = subprocess.PIPE, 94 | universal_newlines=True, text=True, cwd=path) 95 | process.wait() 96 | if process.returncode != 0 and ensure_pass: 97 | raise Exception(process.returncode) 98 | if get_result: 99 | return process.stdout.readlines() 100 | else: 101 | return None 102 | 103 | def prune_existing_sys_deps(deps_list): 104 | remove_deps = [] 105 | for dep in deps_list: 106 | if "node" in dep and which("node"): 107 | remove_deps.append(dep) 108 | elif "npm" in dep and which("npm"): 109 | remove_deps.append(dep) 110 | return list(set(deps_list) - set(remove_deps)) 111 | 112 | def get_archlinux_aur_helper(): 113 | command = None 114 | for helper in ["paru", "pacaur", "yay", "yaourt", "aura"]: 115 | if which(helper): 116 | command = helper 117 | break 118 | if command: 119 | return command 120 | else: 121 | print("Please install one of AUR's helper, such as 'pacaur', 'yay', 'yaourt', 'paru', etc.", file=sys.stderr) 122 | sys.exit(1) 123 | 124 | def install_sys_deps(distro: str, deps_list): 125 | deps_list = prune_existing_sys_deps(deps_list) 126 | command = [] 127 | if distro == 'dnf': 128 | command = ['sudo', 'dnf', '-y', 'install'] 129 | elif distro == 'emerge': 130 | command = ['sudo', 'emerge', "--update"] 131 | elif distro == 'apt': 132 | command = ['sudo', 'apt', '-y', 'install'] 133 | elif distro == 'pacman': 134 | aur_helper = get_archlinux_aur_helper() 135 | command = [aur_helper, '-Sy', '--noconfirm', '--needed'] 136 | elif which("pkg"): 137 | command = ['doas', 'pkg', '-y', 'install'] 138 | elif which("guix"): 139 | command = ['guix', 'install'] 140 | elif which("nix"): 141 | command = ['nix', 'profile', 'install'] 142 | elif which("zypper"): 143 | command = ['sudo', 'zypper', 'install','-y'] 144 | command.extend(deps_list) 145 | try: 146 | run_command(command) 147 | except Exception as e: 148 | print("Error: {}".format(e)) 149 | install_failed_sys.append(' '.join(command)) 150 | 151 | def install_py_deps(deps_list): 152 | if sys.prefix == sys.base_prefix: 153 | # pass --break-system-packages to permit installing packages into EXTERNALLY-MANAGED Python installations. see https://github.com/pypa/pip/issues/11780 154 | if get_distro() != "guix" and os.path.exists(os.path.join(sysconfig.get_path("stdlib", sysconfig.get_default_scheme() if hasattr(sysconfig, "get_default_scheme") else sysconfig._get_default_scheme()),"EXTERNALLY-MANAGED")): 155 | command = [PIP_CMD, 'install', '--user', '--break-system-packages', '-U'] 156 | else: 157 | command = [PIP_CMD, 'install', '--user', '-U'] 158 | else: 159 | # if running on a virtual env, --user option is not valid. 160 | command = [PIP_CMD, 'install', '-U'] 161 | command.extend(deps_list) 162 | try: 163 | run_command(command) 164 | except Exception as e: 165 | print("Error:", e) 166 | install_failed_pys.append(' '.join(command)) 167 | 168 | def install_npm_gloal_deps(deps_list): 169 | command = ["sudo", NPM_CMD, "install", "-g"] 170 | command.extend(deps_list) 171 | 172 | try: 173 | run_command(command) 174 | except Exception as e: 175 | print("Error:", e) 176 | install_failed_npm_globals.append(' '.join(command)) 177 | 178 | def remove_node_modules_path(app_path_list): 179 | for app_path in app_path_list: 180 | node_modules_path = os.path.join(app_path, "node_modules") 181 | if os.path.isdir(node_modules_path): 182 | rmtree(node_modules_path) 183 | print("[EAF] WARN: removing {}".format(node_modules_path)) 184 | 185 | def install_npm_install(app_path_list): 186 | for app_path in app_path_list: 187 | command = [NPM_CMD, "install", "--force"] 188 | try: 189 | run_command(command, path=app_path) 190 | except Exception as e: 191 | print("Error:", e) 192 | install_failed_apps.append(app_path) 193 | 194 | def install_npm_rebuild(app_path_list): 195 | for app_path in app_path_list: 196 | command = [NPM_CMD, "rebuild"] 197 | try: 198 | run_command(command, path=app_path) 199 | except Exception as e: 200 | print("Error:", e) 201 | install_failed_apps.append(app_path) 202 | 203 | def install_vue_install(app_path_list): 204 | for app_path in app_path_list: 205 | command = [NPM_CMD, "install", "--force"] 206 | try: 207 | run_command(command, path=app_path) 208 | except Exception as e: 209 | print("Error:", e) 210 | install_failed_apps.append(app_path) 211 | command = [NPM_CMD, "run", "build"] 212 | try: 213 | run_command(command, path=app_path) 214 | except Exception as e: 215 | print("Error:", e) 216 | install_failed_apps.append(app_path) 217 | 218 | def add_or_update_app(app: str, app_spec_dict): 219 | url = "" 220 | path = os.path.join("app", app) 221 | 222 | if os.path.exists(path): 223 | print("\n[EAF] Updating", app, "to newest version...") 224 | else: 225 | print("\n[EAF] Adding", app, "application to EAF...") 226 | 227 | url = app_spec_dict['url'] 228 | 229 | branch = app_spec_dict['branch'] 230 | time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M') 231 | 232 | 233 | updated = True 234 | if os.path.exists(path): 235 | if args.app_drop_local_edit: 236 | print("[EAF] Clean {}'s local changed for pull code automatically.".format(app)) 237 | run_command(["git", "clean", "-df"], path=path, ensure_pass=False) 238 | run_command(["git", "reset", "--hard", "origin"], path=path, ensure_pass=False) 239 | elif args.app_save_local_edit: 240 | print("[EAF] Clean {}'s local changed for pull code automatically.".format(app)) 241 | run_command(["git", "clean", "-df"], path=path, ensure_pass=False) 242 | run_command(["git", "stash", "save", "[{}] Auto stashed by install-eaf.py".format(time)], path=path, ensure_pass=False) 243 | run_command(["git", "reset", "--hard"], path=path, ensure_pass=False) 244 | run_command(["git", "checkout", branch], path=path, ensure_pass=False) 245 | run_command(["git", "reset", "--hard", "origin"], path=path, ensure_pass=False) 246 | 247 | branch_outputs = run_command(["git", "branch"], path=path, get_result=True) 248 | if branch_outputs is None: 249 | raise Exception("Not in git app!") 250 | exist_branch = False 251 | for b in branch_outputs: 252 | if branch in b: 253 | exist_branch = True 254 | break 255 | if not exist_branch: 256 | run_command(["git", "config", "remote.origin.fetch", "+refs/heads/"+branch+":refs/remotes/origin/"+branch], path=path) 257 | if args.git_full_clone: 258 | run_command(["git", "fetch", "origin", branch], path=path) 259 | else: 260 | run_command(["git", "fetch", "origin", branch, "--depth", "1"], path=path) 261 | 262 | current_branch_outputs = run_command(["git", "symbolic-ref", "HEAD"], path=path, get_result=True) 263 | if current_branch_outputs is None: 264 | raise Exception("git symbolic-ref failed!") 265 | current_branch = current_branch_outputs[0] 266 | if branch not in current_branch: 267 | run_command(["git", "checkout", branch], path=path) 268 | 269 | output_lines = run_command(["git", "pull", "origin", branch], path=path, get_result=True) 270 | if output_lines is None: 271 | raise Exception("git pull failed!") 272 | for output in output_lines: 273 | print(output.rstrip()) 274 | if "Already up to date." in output: 275 | updated = False if branch in current_branch else True 276 | 277 | elif args.git_full_clone: 278 | run_command(["git", "clone", "-b", branch, url, path]) 279 | else: 280 | run_command(["git", "clone", "-b", branch, "--depth", "1", url, path]) 281 | return updated 282 | 283 | def get_distro(): 284 | distro = "" 285 | if sys.platform != "linux": 286 | pass 287 | elif which("dnf"): 288 | distro = "dnf" 289 | elif which("emerge"): 290 | distro = "emerge" 291 | elif which("apt"): 292 | distro = "apt" 293 | elif which("pacman"): 294 | distro = "pacman" 295 | aur_helper = get_archlinux_aur_helper() 296 | if (not args.ignore_core_deps and not args.ignore_sys_deps and len(args.install) == 0) or args.install_core_deps: 297 | try: 298 | run_command([aur_helper, '-Sy', '--noconfirm', '--needed']) 299 | except: 300 | print("Run command `{} -Sy --noconfirm --needed' failed.".format(aur_helper)) 301 | 302 | elif which("pkg"): 303 | distro = "pkg" 304 | elif which("guix"): 305 | distro = "guix" 306 | elif which("zypper"): 307 | distro = "zypper" 308 | elif which("brew"): 309 | distro = "brew" 310 | elif which("nix"): 311 | distro = "nix" 312 | elif sys.platform == "linux": 313 | print("[EAF] Unsupported Linux distribution/package manager.") 314 | print(" Please see dependencies.json for list of dependencies.") 315 | if not (args.ignore_core_deps or args.ignore_sys_deps): 316 | sys.exit(1) 317 | 318 | return distro 319 | 320 | def install_core_deps(distro, deps_dict): 321 | print("[EAF] Installing core dependencies") 322 | core_deps = [] 323 | if not args.ignore_sys_deps and sys.platform == "linux": 324 | core_deps.extend(deps_dict[distro]) 325 | if len(core_deps) > 0: 326 | install_sys_deps(distro, core_deps) 327 | if (not args.ignore_py_deps or sys.platform != "linux") and sys.platform in deps_dict["pip"]: 328 | # For pip dependencies, the distribution name takes precedence over the os name. 329 | # 330 | # For example, in Arch Linux, we need install PyQt from Arch repository to instead install from PIP repository 331 | # to make EAF browser support HTML5 video: 332 | # 333 | # rm -rf ~/.local/lib/python3.10/site-packages/PyQt6* 334 | # sudo rm -rf /usr/lib/python3.10/site-packages/PyQt6* 335 | # sudo pacman -S python-pyqt6-webengine python-pyqt6 python-pyqt6-sip 336 | distro = get_distro() 337 | if distro in deps_dict["pip"]: 338 | install_py_deps(deps_dict["pip"][distro]) 339 | else: 340 | install_py_deps(deps_dict["pip"][sys.platform]) 341 | 342 | print("[EAF] Finished installing core dependencies") 343 | 344 | def yes_no(question, default_yes=False, default_no=False): 345 | key = input(question) 346 | if default_yes: 347 | return key.lower() == 'y' or key == "" 348 | elif default_no: 349 | return key.lower() == 'y' or not (key == "" or key.lower() == 'n') 350 | else: 351 | return key.lower() == 'y' 352 | 353 | def get_installed_apps(app_dir): 354 | apps_installed = [ 355 | f 356 | for f in os.listdir(app_dir) 357 | if os.path.isdir(os.path.join(app_dir, f)) 358 | # directories not managed, or not part of, EAF 359 | if f not in ("__pycache__",) 360 | ] 361 | for app in apps_installed: 362 | git_dir = os.path.join(app_dir, app, ".git") 363 | if app not in get_available_apps_dict().keys(): 364 | apps_installed.remove(app) 365 | if not os.path.isdir(git_dir): 366 | important_messages.append("[EAF] *WARN* 'app/{}' is not a git repo installed by install-eaf.py!".format(app)) 367 | apps_installed.remove(app) 368 | 369 | return apps_installed 370 | 371 | def get_installed_apps_dict(apps_installed): 372 | return {app_name: available_apps_dict[app_name] for app_name in apps_installed} 373 | 374 | def get_new_apps_dict(apps_installed): 375 | not_installed_apps_dict = {} 376 | new_apps_dict = {} 377 | num = 1 378 | for app_name, app_spec_dict in available_apps_dict.items(): 379 | if app_name not in apps_installed: 380 | not_installed_apps_dict[app_name] = app_spec_dict 381 | for app_name, app_spec_dict in not_installed_apps_dict.items(): 382 | indicator = "({}/{})".format(num, len(not_installed_apps_dict)) 383 | prompt = "[EAF] " + indicator + " " + app_spec_dict['name'] + ". " + app_spec_dict['desc'] + " - Install?" 384 | install_p = yes_no(prompt + " (Y/n): ", default_yes=True) if app_spec_dict['default_install'] == 'true' else yes_no(prompt + " (y/N): ", default_no=True) 385 | if install_p: 386 | new_apps_dict[app_name] = app_spec_dict 387 | num = num + 1 388 | return new_apps_dict 389 | 390 | def get_specific_install_apps_dict(apps_need_install): 391 | need_install_apps_dict = {} 392 | for app_name, app_spec_dict in available_apps_dict.items(): 393 | if app_name in apps_need_install: 394 | need_install_apps_dict[app_name] = app_spec_dict 395 | return need_install_apps_dict 396 | 397 | def print_sample_config(app_dir): 398 | for app in get_installed_apps(app_dir): 399 | print("(require 'eaf-{})".format(app)) 400 | 401 | def get_install_apps(apps_installed): 402 | if args.install_all_apps: 403 | return [get_available_apps_dict()] 404 | if len(args.install) > 0: 405 | return [get_specific_install_apps_dict(args.install)] 406 | 407 | pending_apps_dict_list = [get_installed_apps_dict(apps_installed)] 408 | if args.install_new_apps or len(apps_installed) == 0: 409 | pending_apps_dict_list.append(get_new_apps_dict(apps_installed)) 410 | elif not args.install_new_apps: 411 | important_messages.append("[EAF] Use the flag '--install-new-apps' to install new applications.") 412 | 413 | return pending_apps_dict_list 414 | 415 | def install_app_deps(distro, deps_dict): 416 | print("[EAF] Installing application dependencies") 417 | 418 | app_dir = os.path.join(script_path, "app") 419 | if not os.path.exists(app_dir): 420 | os.makedirs(app_dir) 421 | 422 | apps_installed = get_installed_apps(app_dir) 423 | pending_apps_dict_list = get_install_apps(apps_installed) 424 | 425 | sys_deps = [] 426 | py_deps = [] 427 | npm_global_deps = [] 428 | npm_install_apps = [] 429 | vue_install_apps = [] 430 | npm_rebuild_apps = [] 431 | for pending_apps_dict in pending_apps_dict_list: 432 | for app_name, app_spec_dict in pending_apps_dict.items(): 433 | updated = True 434 | try: 435 | updated = add_or_update_app(app_name, app_spec_dict) 436 | except Exception: 437 | raise Exception("[EAF] There are unsaved changes in EAF " + app_name + " application. Please re-run command with --app-drop-local-edit or --app-save-local-edit") 438 | app_path = os.path.join(app_dir, app_name) 439 | app_dep_path = os.path.join(app_path, 'dependencies.json') 440 | if (updated or args.force) and os.path.exists(app_dep_path): 441 | with open(app_dep_path) as f: 442 | deps_dict = json.load(f) 443 | if not args.ignore_sys_deps and sys.platform == "linux" and distro in deps_dict: 444 | sys_deps.extend(deps_dict[distro]) 445 | if not args.ignore_py_deps and 'pip' in deps_dict and sys.platform in deps_dict['pip']: 446 | py_deps.extend(deps_dict['pip'][sys.platform]) 447 | if "npm_global" in deps_dict: 448 | npm_global_deps.extend(deps_dict["npm_global"]) 449 | if not args.ignore_node_deps: 450 | if 'npm_install' in deps_dict and deps_dict['npm_install']: 451 | npm_install_apps.append(app_path) 452 | if 'vue_install' in deps_dict and deps_dict['vue_install']: 453 | vue_install_apps.append(app_path) 454 | if 'npm_rebuild' in deps_dict and deps_dict['npm_rebuild']: 455 | npm_rebuild_apps.append(app_path) 456 | 457 | print("\n[EAF] Installing dependencies for the selected applications") 458 | if not args.ignore_sys_deps and sys.platform == "linux" and len(sys_deps) > 0: 459 | print("[EAF] Installing system dependencies") 460 | install_sys_deps(distro, sys_deps) 461 | if not args.ignore_py_deps and len(py_deps) > 0: 462 | print("[EAF] Installing python dependencies") 463 | install_py_deps(py_deps) 464 | if len(npm_global_deps) > 0: 465 | install_npm_gloal_deps(npm_global_deps) 466 | if not args.ignore_node_deps: 467 | if args.force: 468 | if len(npm_install_apps) > 0: 469 | remove_node_modules_path(npm_install_apps) 470 | if len(vue_install_apps) > 0: 471 | remove_node_modules_path(vue_install_apps) 472 | if len(npm_install_apps) > 0: 473 | install_npm_install(npm_install_apps) 474 | if len(npm_rebuild_apps) > 0: 475 | install_npm_rebuild(npm_rebuild_apps) 476 | if len(vue_install_apps) > 0: 477 | install_vue_install(vue_install_apps) 478 | 479 | print("\n[EAF] Please always ensure the following config are added to your init.el:") 480 | print_sample_config(app_dir) 481 | 482 | global install_failed_sys 483 | global install_failed_pys 484 | global install_failed_npm_globals 485 | global install_failed_apps 486 | if len(install_failed_sys) > 0: 487 | install_failed_sys = list(set(install_failed_sys)) 488 | print_warning_message("\n[EAF] Installation FAILED for the following system dependencies:") 489 | for dep in install_failed_sys: 490 | print_warning_message(dep) 491 | if len(install_failed_pys) > 0: 492 | install_failed_pys = list(set(install_failed_pys)) 493 | print_warning_message("\n[EAF] Installation FAILED for the following Python dependencies:") 494 | for dep in install_failed_pys: 495 | print_warning_message(dep) 496 | if len(install_failed_npm_globals) > 0: 497 | install_failed_npm_globals = list(set(install_failed_npm_globals)) 498 | print_warning_message("\n[EAF] Installation FAILED for the following NPM dependencies:") 499 | for dep in install_failed_npm_globals: 500 | print_warning_message(dep) 501 | if len(install_failed_apps) > 0: 502 | install_failed_apps = list(set(install_failed_apps)) 503 | print_warning_message("\n[EAF] Installation FAILED for following applications:") 504 | for app in install_failed_apps: 505 | print_warning_message(app) 506 | if len(install_failed_sys) + len(install_failed_pys) + len(install_failed_apps) == 0: 507 | print("[EAF] Installation SUCCESS!") 508 | else: 509 | print("[EAF] Please rerun ./install-eaf.py with `--force`, or install them manually!") 510 | 511 | def print_warning_message(message): 512 | print(bcolors.WARNING + message + bcolors.ENDC) 513 | 514 | def main(): 515 | try: 516 | distro = get_distro() 517 | with open(os.path.join(script_path, 'dependencies.json')) as f: 518 | deps_dict = json.load(f) 519 | 520 | if (not args.ignore_core_deps and len(args.install) == 0) or args.install_core_deps: 521 | print("[EAF] ------------------------------------------") 522 | install_core_deps(distro, deps_dict) 523 | print("[EAF] ------------------------------------------") 524 | 525 | if not args.install_core_deps: 526 | print("[EAF] ------------------------------------------") 527 | install_app_deps(distro, deps_dict) 528 | print("[EAF] ------------------------------------------") 529 | 530 | current_desktop = os.getenv("XDG_CURRENT_DESKTOP") or os.getenv("XDG_SESSION_DESKTOP") 531 | if current_desktop in ["Hyprland", "sway"]: 532 | print("[EAF] Compiling reinput") 533 | subprocess.Popen("gcc reinput/main.c -o reinput/reinput `pkg-config --cflags --libs libinput libevdev libudev`", 534 | shell=True) 535 | 536 | print("[EAF] install-eaf.py finished.\n") 537 | 538 | for msg in important_messages: 539 | print_warning_message(msg) 540 | except KeyboardInterrupt: 541 | print("[EAF] install-eaf.py aborted!") 542 | sys.exit() 543 | 544 | 545 | if __name__ == '__main__': 546 | main() 547 | --------------------------------------------------------------------------------