├── .env ├── .gitignore ├── LICENSE.txt ├── Readme.md ├── craco.config.js ├── environment.yml ├── main ├── index.ts └── with-python.ts ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── python ├── .gitignore ├── api.py └── calc.py ├── src ├── App.css ├── App.tsx ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts └── serviceWorker.ts ├── tsconfig.electronMain.json ├── tsconfig.json └── tslint.json /.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | buildMain 3 | dist 4 | node_modules 5 | pythondist 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Don Alvarez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Electron + Python 2 | 3 | This sample shows how to build Python Flask apps that run in Electron. I'm using it to combine Python backends with React frontends but if you prefer to use handle your frontend entirely from Flask that should be simple to do. The Electron main (backend) process spawns a Python Flask webserver and provides a randomly generated authentication token to both the webserver and the Electron renderer (frontend) process for use in authenticating messages sent between the frontend and the webserver. 4 | 5 | The webserver currently exposes a GraphQL endpoint for the frontend to interact with but the backend is just a plain old Flask webserver so you can tweak it to host whatever sort of REST or other Flask web services as might be needed by your application. The React frontend part of the sample is similarly based on a stock create-react-app site, so it should be easy to customize as needed. The only significant embelishments to the stock cra app are (1) the bare minimal amount of https://github.com/sharegate/craco to support hooking into electron without needing to eject the create react app and (2) typescript support, which you don't have to use but I personally can't imagine building a serious javascript project without it so it's there if you need it. 6 | 7 | This example builds a stand-alone Electron + Create-React-App + Python application and installer. On Windows it builds the app into `./dist/win-unpacked/My Electron Python App.exe` and the installer into `./dist/My Electron Python App Setup 1.0.0.exe` (OSX and Linux destinations are similar). You can change the name of the application by changing the `name` property in `package.json`. 8 | 9 | # Installation 10 | 11 | Tested with Anaconda Python v3, should work fine with Anaconda Python v2 (should also work fine with whatever python environment you use if you have the correct packages installed). 12 | 13 | NOTE: On windows you will need to [install anaconda](https://www.anaconda.com/download/) (which installs python and pip) and potentially configure environment variables to add python and/or pip to the path if you don't have it installed already. 14 | 15 | ```bash 16 | # start with the obvious step you always need to do with node projects 17 | npm install 18 | 19 | # Depending on the packages you install, with Electron projects you may need to do 20 | # an npm rebuild to rebuild any included binaries for the current OS. It's probably 21 | # not needed here but I do it out of habit because its fast and the issues can be 22 | # a pain to track down if they come up and you dont realize a rebuild is needed 23 | npm rebuild 24 | ``` 25 | 26 | **VERY IMPORTANT:** Windows users, if you use VS Code or use Powershell as your shell, you need to type `cmd` inside the VS Code terminal or inside your Powershell window before running the conda commands because conda's environment switcher will not work under Powershell (much of it works, but the critical parts that don't work, like activating evironments, fail silently while appearing to work), 27 | 28 | ```bash 29 | # install Anaconda if not already installed 30 | 31 | cmd # Only needed if you're coding on Windows in VS Code or Powershell, as discussed above 32 | conda env create -f environment.yml 33 | conda activate electron-python-sample 34 | conda env list 35 | # in the list, make sure the electron-python-sample has a * in front 36 | # indicating it is activated (under Powershell on Windows the activate 37 | # command fails silently which is why you needed to run the conda commands 38 | # in a cmd prompt) 39 | 40 | # run the unpackaged python scripts from a dev build of electron 41 | npm run start # must be run in the same shell you just conda activated 42 | ``` 43 | 44 | **NOTE** if you see the following error message when trying to `npm run start` it means you did not successfully `conda activate electron-python-sample` in the shell from which you are trying to `npm run start`. On Windows under VS Code that could be because you forgot to go into a `cmd` shell as discussed above before trying to conda activate. 45 | 46 | ``` 47 | Traceback (most recent call last): 48 | File "python/api.py", line 3, in 49 | from graphene import ObjectType, String, Schema 50 | ModuleNotFoundError: No module named 'graphene' 51 | ``` 52 | 53 | ```bash 54 | # use pyinstaller to convert the source code in python/ into an 55 | # executable in pythondist/, build the electron app into a subdirectory 56 | # of dist/, and run electron-packager to package the electron app as a 57 | # platform-specific installer in dist/ 58 | npm run build # must be run in the same shell you just conda activated 59 | 60 | # double-click to run the either the platform-specific app that is built 61 | # into a subdirectory of dist/ or the platform-specific installer that is 62 | # built and placed in the dist/ folder 63 | ``` 64 | 65 | # Debugging the Python process 66 | 67 | To test the Python GraphQL server, in a conda activated terminal window run `npm run python-build`, cd into the newly generated `pythondist` folder, and run `api.exe --apiport 5000 --signingkey devkey` then browse to `http://127.0.0.1:5000/graphiql/` to access a GraphiQL view of the server. For a more detailed example, try `http://127.0.0.1:5000/graphiql/?query={calc(math:"1/2",signingkey:"devkey")}` which works great if you copy and paste into the browser but which is a complex enough URL that it will confuse chrome if you try to click directly on it. 68 | 69 | # Notes 70 | 71 | The electron main process both spawns the Python child process and creates the window. The electron renderer process communicates with the python backend via GraphQL web service calls. 72 | 73 | The Python script `python/calc.py` provides a function: `calc(text)` that can take text like `1 + 1` and return the result like `2.0`. The calc functionality is exposed as a GraphQL api by `python/api.py`. 74 | 75 | The details of how the electron app launches the Python executable is tricky because of differences between packaged and unpackaged scenarios. This complexity is handled by `main/with-python.ts`. If the Electron app is not packaged, the code needs to `spawn` the Python source script. If the Electron app is packaged, it needs to `execFile` the packaged Python executable found in the app.asar. To decide whether the Electron app itself has been packaged for distribution or not, `main/with-python.ts` checks whether the `__dirname` looks like an asar folder or not. 76 | 77 | # Important 78 | 79 | Killing spawned processes under Electron can be tricky so the electron main process sends a message to the Python server telling it to exit when Electron is shutting down (and yes, that does mean that if you are debugging and control-c to kill the npm process hosting the app you can leave a zombie python process, so it's better to close the app normally by closing the window before killing your npm process). 80 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: { 3 | configure: { 4 | target: 'electron-renderer' 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: electron-python-sample 2 | channels: 3 | - fastai 4 | - peterjc123 5 | - pytorch 6 | - defaults 7 | dependencies: 8 | - certifi=2018.10.15=py37_0 9 | - cffi=1.11.5=py37h74b6da3_1 10 | - click=7.0=py37_0 11 | - flask=1.0.2=py37_1 12 | - future=0.16.0=py37_0 13 | - gevent=1.3.7=py37he774522_1 14 | - greenlet=0.4.15=py37hfa6e2cd_0 15 | - itsdangerous=1.1.0=py37_0 16 | - jinja2=2.10=py37_0 17 | - markupsafe=1.0=py37hfa6e2cd_1 18 | - msgpack-python=0.5.6=py37he980bc4_1 19 | - pip=10.0.1=py37_0 20 | - pycparser=2.19=py37_0 21 | - python=3.7.1=h33f27b4_4 22 | - setuptools=40.4.3=py37_0 23 | - werkzeug=0.14.1=py37_0 24 | - wheel=0.32.2=py37_0 25 | - wincertstore=0.2=py37_0 26 | - vc=14.1=h21ff451_1 27 | - vs2017_runtime=15.4.27004.2010=1 28 | - pip: 29 | - flask-cors 30 | - flask-graphql 31 | - graphene 32 | - pyinstaller 33 | - pypiwin32 34 | -------------------------------------------------------------------------------- /main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from "electron"; // tslint:disable-line 2 | import * as path from "path"; 3 | import "./with-python"; 4 | 5 | const isDev = (process.env.NODE_ENV === "development"); 6 | 7 | app.on("window-all-closed", () => { 8 | if (process.platform !== "darwin") { 9 | app.quit(); 10 | } 11 | }); 12 | 13 | app.on("ready", () => { 14 | if (isDev) { 15 | const sourceMapSupport = require("source-map-support"); // tslint:disable-line 16 | sourceMapSupport.install(); 17 | } 18 | createWindow(); 19 | }); 20 | 21 | function createWindow() { 22 | const win = new BrowserWindow(); 23 | win.webContents.openDevTools(); 24 | if (isDev) { 25 | win.loadURL("http://localhost:3000/index.html"); 26 | } else { 27 | win.loadURL(`file://${path.join(__dirname, "/../build/index.html")}`); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /main/with-python.ts: -------------------------------------------------------------------------------- 1 | import childProcess from "child_process"; 2 | import crossSpawn from "cross-spawn"; 3 | import Electron, { app, dialog, ipcMain } from "electron"; // tslint:disable-line 4 | import fs from "fs"; 5 | import getPort from "get-port"; 6 | import * as path from "path"; 7 | import superagent from "superagent"; 8 | import uuid from "uuid"; 9 | 10 | const PY_DIST_FOLDER = "pythondist"; 11 | const PY_FOLDER = "python"; 12 | const PY_MODULE = "api"; // without .py suffix 13 | 14 | const isDev = (process.env.NODE_ENV === "development"); 15 | 16 | let pyProc = null as any; 17 | 18 | const apiDetails = { 19 | port:0, 20 | signingKey:"", 21 | }; 22 | 23 | const initializeApi = async () => { 24 | // dialog.showErrorBox("success", "initializeApi"); 25 | const availablePort = await getPort(); 26 | apiDetails.port = isDev ? 5000 : availablePort; 27 | const key = isDev ? "devkey" : uuid.v4(); 28 | apiDetails.signingKey = key; 29 | const srcPath = path.join(__dirname, "..", PY_FOLDER, PY_MODULE + ".py"); 30 | const exePath = (process.platform === "win32") ? path.join(__dirname, "..", PY_DIST_FOLDER, PY_MODULE + ".exe") : path.join(__dirname, PY_DIST_FOLDER, PY_MODULE); 31 | if (__dirname.indexOf("app.asar") > 0) { 32 | // dialog.showErrorBox("info", "packaged"); 33 | if (fs.existsSync(exePath)) { 34 | pyProc = childProcess.execFile(exePath, ["--apiport", String(apiDetails.port), "--signingkey", apiDetails.signingKey], {}, (error, stdout, stderr) => { 35 | if (error) { 36 | console.log(error); 37 | console.log(stderr); 38 | } 39 | }); 40 | if (pyProc === undefined) { 41 | dialog.showErrorBox("Error", "pyProc is undefined"); 42 | dialog.showErrorBox("Error", exePath); 43 | } else if (pyProc === null) { 44 | dialog.showErrorBox("Error", "pyProc is null"); 45 | dialog.showErrorBox("Error", exePath); 46 | } 47 | } else { 48 | dialog.showErrorBox("Error", "Packaged python app not found"); 49 | } 50 | } else { 51 | // dialog.showErrorBox("info", "unpackaged"); 52 | if (fs.existsSync(srcPath)) { 53 | pyProc = crossSpawn("python", [srcPath, "--apiport", String(apiDetails.port), "--signingkey", apiDetails.signingKey]); 54 | } else { 55 | dialog.showErrorBox("Error", "Unpackaged python source not found"); 56 | } 57 | } 58 | if (pyProc === null || pyProc === undefined) { 59 | dialog.showErrorBox("Error", "unable to start python server"); 60 | } else { 61 | console.log("Server running at http://127.0.0.1:" + apiDetails.port); 62 | } 63 | console.log("leaving initializeApi()"); 64 | }; 65 | 66 | ipcMain.on("getApiDetails", (event:Electron.Event) => { 67 | if (apiDetails.signingKey !== "") { 68 | event.sender.send("apiDetails", JSON.stringify(apiDetails)); 69 | } else { 70 | initializeApi() 71 | .then(() => { 72 | event.sender.send("apiDetails", JSON.stringify(apiDetails)); 73 | }) 74 | .catch(() => { 75 | event.sender.send("apiDetailsError", "Error initializing API"); 76 | }); 77 | } 78 | }); 79 | 80 | const exitPyProc = () => { 81 | // 82 | // NOTE: killing processes in node is surprisingly tricky and a simple 83 | // pyProc.kill() totally isn't enough. Instead send a message to 84 | // the pyProc web server telling it to exit 85 | // 86 | superagent.get("http://127.0.0.1:" + apiDetails.port + "/graphql/?query=%7Bexit(signingkey:\"" + apiDetails.signingKey + "\")%7D").then().catch(); 87 | pyProc = null; 88 | }; 89 | 90 | app.on("will-quit", exitPyProc); 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-python", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "./", 6 | "main": "buildMain/index.js", 7 | "scripts": { 8 | "lint": "npm run main-lint && npm run react-lint", 9 | "start": "concurrently \"npm run react-start\" \"npm run main-start\"", 10 | "watch": "concurrently \"npm run main-watch\" \"npm run react-watch\"", 11 | "build": "npm run python-build && npm run react-build && npm run main-build", 12 | "main-lint": "tslint -c tslint.json -p tsconfig.electronMain.json", 13 | "main-start": "tsc -p tsconfig.electronMain.json && wait-on http://localhost:3000/ && cross-env NODE_ENV=development electron .", 14 | "main-watch": "tsc -w -p tsconfig.electronMain.json", 15 | "main-build": "tsc -p tsconfig.electronMain.json && electron-builder", 16 | "react-lint": "tslint -c tslint.json -p tsconfig.json", 17 | "react-start": "craco start", 18 | "react-watch": "tsc -w -p tsconfig.json", 19 | "react-build": "craco build", 20 | "python-build": "shx rm -rf pythondist && pyinstaller python/api.py --distpath pythondist --workpath build-py-temp --onefile && shx rm -rf build-py-temp && shx rm api.spec" 21 | }, 22 | "dependencies": { 23 | "@craco/craco": "^3.5.0", 24 | "@types/cross-spawn": "^6.0.0", 25 | "@types/isomorphic-fetch": "0.0.35", 26 | "@types/node": "^11.11.3", 27 | "@types/react": "^16.8.8", 28 | "@types/react-dom": "^16.8.2", 29 | "@types/superagent": "^4.1.1", 30 | "@types/uuid": "^3.4.4", 31 | "apollo-cache-inmemory": "^1.5.1", 32 | "apollo-client": "^2.5.1", 33 | "apollo-link-http": "^1.5.14", 34 | "cross-spawn": "^6.0.5", 35 | "get-port": "^4.2.0", 36 | "graphql": "^14.2.0", 37 | "graphql-tag": "^2.10.1", 38 | "isomorphic-fetch": "^2.2.1", 39 | "react": "^16.8.4", 40 | "react-dom": "^16.8.4", 41 | "react-scripts": "3.0.1", 42 | "shx": "^0.3.2", 43 | "superagent": "^5.0.2", 44 | "typescript": "^3.3.3333", 45 | "uuid": "^3.3.2" 46 | }, 47 | "devDependencies": { 48 | "concurrently": "^4.1.0", 49 | "cross-env": "^5.2.0", 50 | "electron": "4.1.0", 51 | "electron-builder": "^20.39.0", 52 | "source-map-support": "^0.5.11", 53 | "tslint": "^5.9.1", 54 | "tslint-config-standard": "^7.0.0", 55 | "tslint-loader": "^3.5.3", 56 | "tslint-react": "^3.4.0", 57 | "wait-on": "^3.2.0" 58 | }, 59 | "eslintConfig": { 60 | "extends": "react-app" 61 | }, 62 | "browserslist": [ 63 | ">0.2%", 64 | "not dead", 65 | "not ie <= 11", 66 | "not op_mini all" 67 | ], 68 | "build": { 69 | "appId": "com.example.electron-python", 70 | "productName": "My Electron Python App", 71 | "forceCodeSigning": false, 72 | "directories": { 73 | "buildResources": "build", 74 | "output": "dist" 75 | }, 76 | "extraMetadata": { 77 | "main": "buildMain/index.js" 78 | }, 79 | "files": [ 80 | "node_modules/**", 81 | "buildMain/**", 82 | "pythondist/**" 83 | ], 84 | "extraResources": [], 85 | "extraFiles": [], 86 | "mac": { 87 | "target": "dmg" 88 | }, 89 | "mas": {}, 90 | "dmg": {}, 91 | "pkg": {}, 92 | "win": { 93 | "target": "nsis" 94 | }, 95 | "nsis": {}, 96 | "nsisWeb": {}, 97 | "portable": {}, 98 | "appx": {}, 99 | "squirrelWindows": {} 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoDon/electron-python/6593556b5d2265f54081fdc70794c85726060850/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /python/api.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_cors import CORS 3 | from graphene import ObjectType, String, Schema 4 | from flask_graphql import GraphQLView 5 | from calc import calc as real_calc 6 | import argparse 7 | import os 8 | 9 | # 10 | # Notes on setting up a flask GraphQL server 11 | # https://codeburst.io/how-to-build-a-graphql-wrapper-for-a-restful-api-in-python-b49767676630 12 | # 13 | # Notes on using pyinstaller to package a flask server (discussing issues that don't come up 14 | # in this simple example but likely would come up in a more real application) 15 | # for making pyinstaller see https://mapopa.blogspot.com/2013/10/flask-and-pyinstaller-notice.html 16 | # and https://github.com/pyinstaller/pyinstaller/issues/1071 17 | # and https://elc.github.io/posts/executable-flask-pyinstaller/ 18 | # 19 | 20 | class Query(ObjectType): 21 | 22 | # 23 | # IMPORTANT - There is currently nothing preventing a malicious web page 24 | # running in the users web browser from making requests of this 25 | # server. If you add additional code here you will need to make 26 | # sure its either code that is appropriate for a malicious web 27 | # page to be able to run (like the calculator example below) or 28 | # that you wrap some kind of security model around the python 29 | # web server before adding the code. 30 | # 31 | 32 | awake = String(description="Awake") 33 | def resolve_awake(self, args): 34 | return "Awake" 35 | 36 | exit = String(description="Exit", signingkey=String(required=True)) 37 | def resolve_exit(self, info, signingkey): 38 | if signingkey != apiSigningKey: 39 | return 40 | os._exit(0) 41 | return 42 | 43 | hello = String(description="Hello", signingkey=String(required=True)) 44 | def resolve_hello(self, info, signingkey): 45 | if signingkey != apiSigningKey: 46 | return "invalid signature" 47 | return "World" 48 | 49 | calc = String(description="Calculator", signingkey=String(required=True), math=String(required=True)) 50 | def resolve_calc(self, info, signingkey, math): 51 | """based on the input text, return the int result""" 52 | if signingkey != apiSigningKey: 53 | return "invalid signature" 54 | try: 55 | return real_calc(math) 56 | except Exception: 57 | return 0.0 58 | 59 | echo = String(description="Echo", signingkey=String(required=True), text=String(required=True)) 60 | def resolve_echo(self, info, signingkey, text): 61 | if signingkey != apiSigningKey: 62 | return "invalid signature" 63 | """echo any text""" 64 | return text 65 | 66 | view_func = GraphQLView.as_view("graphql", schema=Schema(query=Query), graphiql=True) 67 | 68 | parser = argparse.ArgumentParser() 69 | parser.add_argument("--apiport", type=int, default=5000) 70 | parser.add_argument("--signingkey", type=str, default="") 71 | args = parser.parse_args() 72 | 73 | apiSigningKey = args.signingkey 74 | 75 | app = Flask(__name__) 76 | app.add_url_rule("/graphql/", view_func=view_func) 77 | app.add_url_rule("/graphiql/", view_func=view_func) # for compatibility with other samples 78 | CORS(app) # Allows all domains to access the flask server via CORS 79 | 80 | if __name__ == "__main__": 81 | app.run(port=args.apiport) 82 | -------------------------------------------------------------------------------- /python/calc.py: -------------------------------------------------------------------------------- 1 | """ 2 | A calculator based on https://en.wikipedia.org/wiki/Reverse_Polish_notation 3 | """ 4 | 5 | from __future__ import print_function 6 | 7 | 8 | def getPrec(c): 9 | if c in "+-": 10 | return 1 11 | if c in "*/": 12 | return 2 13 | if c in "^": 14 | return 3 15 | return 0 16 | 17 | def getAssoc(c): 18 | if c in "+-*/": 19 | return "LEFT" 20 | if c in "^": 21 | return "RIGHT" 22 | return "LEFT" 23 | 24 | def getBin(op, a, b): 25 | if op == '+': 26 | return a + b 27 | if op == '-': 28 | return a - b 29 | if op == '*': 30 | return a * b 31 | if op == '/': 32 | return a / b 33 | if op == '^': 34 | return a ** b 35 | return 0 36 | 37 | def calc(s): 38 | numStk = [] 39 | opStk = [] 40 | i = 0 41 | isUnary = True 42 | while (i < len(s)): 43 | while (i < len(s) and s[i] == ' '): 44 | i += 1 45 | if (i >= len(s)): 46 | break 47 | if (s[i].isdigit()): 48 | num = '' 49 | while (i < len(s) and (s[i].isdigit() or s[i] == '.')): 50 | num += s[i] 51 | i += 1 52 | numStk.append(float(num)) 53 | isUnary = False 54 | continue 55 | 56 | if (s[i] in "+-*/^"): 57 | if isUnary: 58 | opStk.append('#') 59 | else: 60 | while (len(opStk) > 0): 61 | if ((getAssoc(s[i]) == "LEFT" and getPrec(s[i]) <= getPrec(opStk[-1])) or 62 | (getAssoc(s[i]) == "RIGHT" and getPrec(s[i]) < getPrec(opStk[-1]))): 63 | op = opStk.pop() 64 | if op == '#': 65 | numStk.append(-numStk.pop()) 66 | else: 67 | b = numStk.pop() 68 | a = numStk.pop() 69 | numStk.append(getBin(op, a, b)) 70 | continue 71 | break 72 | opStk.append(s[i]) 73 | isUnary = True 74 | elif (s[i] == '('): 75 | opStk.append(s[i]) 76 | isUnary = True 77 | else: 78 | while (len(opStk) > 0): 79 | op = opStk.pop() 80 | if (op == '('): 81 | break 82 | if op == '#': 83 | numStk.append(-numStk.pop()) 84 | else: 85 | b = numStk.pop() 86 | a = numStk.pop() 87 | numStk.append(getBin(op, a, b)) 88 | i += 1 89 | 90 | while (len(opStk) > 0): 91 | op = opStk.pop() 92 | if op == '#': 93 | numStk.append(-numStk.pop()) 94 | else: 95 | b = numStk.pop() 96 | a = numStk.pop() 97 | numStk.append(getBin(op, a, b)) 98 | 99 | return numStk.pop() 100 | 101 | 102 | if __name__ == '__main__': 103 | ss = [ 104 | "1 + 2 * 3 / 4 - 5 + - 6", # -8.5 105 | "10 + ( - 1 ) ^ 4", # 11 106 | "10 + - 1 ^ 4", # 9 107 | "10 + - - 1 ^ 4", # 11 108 | "10 + - ( - 1 ^ 4 )", # 11 109 | "5 * ( 10 - 9 )", # 5 110 | "1 + 2 * 3", # 7 111 | "4 ^ 3 ^ 2", # 262144 112 | "4 ^ - 3", # 0.015625 113 | "4 ^ ( - 3 )", # 0.015625 114 | ] 115 | for s in ss: 116 | res = calc(s) 117 | print('{} = {}'.format(res, s)) 118 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { InMemoryCache } from "apollo-cache-inmemory"; 2 | import { ApolloClient } from "apollo-client"; 3 | import { HttpLink } from "apollo-link-http"; 4 | import gql from "graphql-tag"; 5 | import fetch from "isomorphic-fetch"; 6 | import React, { useMemo, useState } from "react"; 7 | import "./App.css"; 8 | import logo from "./logo.svg"; 9 | 10 | const ipcRenderer = (window as any).isInElectronRenderer 11 | ? (window as any).nodeRequire("electron").ipcRenderer 12 | : (window as any).ipcRendererStub; 13 | 14 | const App = () => { 15 | const [mathResult, setMathResult] = useState(""); 16 | const [apiPort, setApiPort] = useState(0); 17 | const [apiSigningKey, setApiSigningKey] = useState(""); 18 | 19 | const appGlobalClient = useMemo(() => { 20 | if (apiPort === 0) { 21 | if (ipcRenderer) { 22 | ipcRenderer.on("apiDetails", ({}, argString:string) => { 23 | const arg:{ port:number, signingKey:string } = JSON.parse(argString); 24 | setApiPort(arg.port); // setting apiPort causes useMemo'd appGlobalClient to be re-evaluated 25 | setApiSigningKey(arg.signingKey); 26 | }); 27 | ipcRenderer.send("getApiDetails"); 28 | } 29 | return null; 30 | } 31 | return new ApolloClient({ 32 | cache: new InMemoryCache(), 33 | link: new HttpLink({ 34 | fetch:(fetch as any), 35 | uri: "http://127.0.0.1:" + apiPort + "/graphql/", 36 | }), 37 | }); 38 | }, [apiPort]); 39 | 40 | const handleKeyDown = (event:React.KeyboardEvent) => { 41 | if (event.key === "Enter") { 42 | const math = event.currentTarget.value; 43 | if (appGlobalClient === null) { 44 | setMathResult("this page only works when hosted in electron"); 45 | return; 46 | } 47 | appGlobalClient.query({ 48 | query:gql`query calc($signingkey:String!, $math:String!) { 49 | calc(signingkey:$signingkey, math:$math) 50 | }`, 51 | variables: { 52 | math, 53 | signingkey: apiSigningKey, 54 | }, 55 | }) 56 | .then(({ data }) => { 57 | setMathResult(data.calc); 58 | }) 59 | .catch((e) => { 60 | console.log("Error contacting graphql server"); 61 | console.log(e); 62 | setMathResult("Error getting result with port=" + apiPort + " and signingkey='" + apiSigningKey + "' (if you launched via npm run start, please check readme about conda activate and cmd. Also, if this is the first call, the server may need a few seconds to initialize)"); 63 | }); 64 | } 65 | }; 66 | 67 | return ( 68 |
69 |
70 | logo 71 |

72 | Edit src/App.tsx and save to reload. 73 |

74 |

Input something like 1 + 1.

75 |

76 | This calculator supports +-*/^(), 77 | whitespaces, and integers and floating numbers. 78 |

79 | 83 |
84 | {mathResult} 85 |
86 |
87 |
88 | ); 89 | }; 90 | 91 | export default App; 92 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === "localhost" || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === "[::1]" || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 20 | ), 21 | ); 22 | 23 | interface IConfig { 24 | onSuccess?:(registration:ServiceWorkerRegistration) => void; 25 | onUpdate?:(registration:ServiceWorkerRegistration) => void; 26 | } 27 | 28 | export function register(config?:IConfig) { 29 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env:{ [key:string]:string } }).env.PUBLIC_URL, 33 | window.location.href, 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won"t work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener("load", () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let"s check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log("This web app is being served cache-first by a service worker. To learn more, visit https://bit.ly/CRA-PWA"); 53 | }) 54 | .catch((error) => { 55 | console.log(error); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl:string, config?:IConfig) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then((registration) => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker === null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === "installed") { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log("New content is available and will be used when all tabs for this page are closed. See https://bit.ly/CRA-PWA."); 81 | 82 | // Execute callback 83 | if (config && config.onUpdate) { 84 | config.onUpdate(registration); 85 | } 86 | } else { 87 | // At this point, everything has been precached. 88 | // It"s the perfect time to display a 89 | // "Content is cached for offline use." message. 90 | console.log("Content is cached for offline use."); 91 | 92 | // Execute callback 93 | if (config && config.onSuccess) { 94 | config.onSuccess(registration); 95 | } 96 | } 97 | } 98 | }; 99 | }; 100 | }) 101 | .catch((error) => { 102 | console.error("Error during service worker registration:", error); 103 | }); 104 | } 105 | 106 | function checkValidServiceWorker(swUrl:string, config?:IConfig) { 107 | // Check if the service worker can be found. If it can"t reload the page. 108 | fetch(swUrl) 109 | .then((response) => { 110 | // Ensure service worker exists, and that we really are getting a JS file. 111 | const contentType = response.headers.get("content-type"); 112 | if ( 113 | response.status === 404 || 114 | (contentType !== null && contentType.indexOf("javascript") === -1) 115 | ) { 116 | // No service worker found. Probably a different app. Reload the page. 117 | navigator.serviceWorker.ready.then((registration) => { 118 | registration.unregister().then(() => { 119 | window.location.reload(); 120 | }) 121 | .catch((error) => { 122 | console.log(error); 123 | }); 124 | }) 125 | .catch((error) => { 126 | console.log(error); 127 | }); 128 | } else { 129 | // Service worker found. Proceed as normal. 130 | registerValidSW(swUrl, config); 131 | } 132 | }) 133 | .catch(() => { 134 | console.log("No internet connection found. App is running in offline mode."); 135 | }); 136 | } 137 | 138 | export function unregister() { 139 | if ("serviceWorker" in navigator) { 140 | navigator.serviceWorker.ready.then((registration) => { 141 | registration.unregister() 142 | .catch((error) => { 143 | console.log(error); 144 | }); 145 | }) 146 | .catch((error) => { 147 | console.log(error); 148 | }); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tsconfig.electronMain.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": [ 10 | "dom", 11 | "dom.iterable", 12 | "esnext" 13 | ], 14 | "moduleResolution": "node", 15 | "noImplicitAny": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "target": "es5", 20 | 21 | "baseUrl": ".", 22 | "module": "commonjs", 23 | "outDir": "buildMain", 24 | "paths": { 25 | "*": ["node_modules/*"] 26 | }, 27 | "sourceMap": true 28 | }, 29 | "include": [ 30 | "main", 31 | ], 32 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": [ 10 | "dom", 11 | "dom.iterable", 12 | "esnext" 13 | ], 14 | "moduleResolution": "node", 15 | "noImplicitAny": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "target": "es5", 20 | 21 | "module": "esnext", 22 | "noEmit": true, 23 | }, 24 | "include": [ 25 | "src" 26 | ], 27 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint:latest", 5 | "tslint-react" 6 | ], 7 | "rules": { 8 | "no-console": false, 9 | "indent": [true, "spaces"], 10 | "max-line-length": false, 11 | "space-before-function-paren": false, 12 | "ter-indent": [true, 4], 13 | "whitespace": [ 14 | true, 15 | "check-branch", 16 | "check-decl", 17 | "check-operator", 18 | "check-module", 19 | "check-separator", 20 | "check-rest-spread", 21 | "check-typecast", 22 | "check-type-operator", 23 | "check-preblock" 24 | ], 25 | "typedef-whitespace": [ 26 | true, 27 | { 28 | "call-signature": "nospace", 29 | "index-signature": "nospace", 30 | "parameter": "nospace", 31 | "property-declaration": "nospace", 32 | "variable-declaration": "nospace" 33 | }, 34 | { 35 | "call-signature": "nospace", 36 | "index-signature": "nospace", 37 | "parameter": "nospace", 38 | "property-declaration": "nospace", 39 | "variable-declaration": "nospace" 40 | } 41 | ] 42 | } 43 | } --------------------------------------------------------------------------------