├── .gitignore
├── resources
├── icon.png
└── screenshot.png
├── .babelrc
├── src
├── helpers
│ ├── file-manager.js
│ ├── app_cache.js
│ ├── window.js
│ └── menu.js
└── main.js
├── scripts
├── start.js
└── remix-dl.js
├── webpack.config.js
├── README.md
├── shell.nix
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 | build
5 |
--------------------------------------------------------------------------------
/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/horizon-games/remix-app/HEAD/resources/icon.png
--------------------------------------------------------------------------------
/resources/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/horizon-games/remix-app/HEAD/resources/screenshot.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/env", {
5 | "targets": {
6 | "browsers": "last 2 Chrome versions",
7 | "node": "current"
8 | }
9 | }
10 | ]
11 | ],
12 | "plugins": ["@babel/plugin-proposal-object-rest-spread"]
13 | }
14 |
--------------------------------------------------------------------------------
/src/helpers/file-manager.js:
--------------------------------------------------------------------------------
1 | import { ipcMain, dialog, BrowserWindow } from 'electron';
2 | import fs from 'fs';
3 |
4 | ipcMain.on('save-file', (event, file) => {
5 | const win = BrowserWindow.getFocusedWindow()
6 | dialog.showSaveDialog(win, {
7 | title: 'Save solidity file',
8 | defaultPath: file.title,
9 | filters: [{ name: 'Solidity Smart Contract', extensions: ['sol'] }]
10 | }, (filePath) => {
11 | if(filePath){
12 | fs.writeFile(filePath, file.content, (err) => {
13 | if(err) alert(err);
14 | })
15 | }
16 | });
17 | });
18 |
19 | export { saveFile };
20 |
--------------------------------------------------------------------------------
/scripts/start.js:
--------------------------------------------------------------------------------
1 | const { spawnSync } = require('child_process')
2 | const childProcess = require('child_process')
3 | const webpack = require('webpack')
4 | const config = require('../webpack.config')
5 | const electron = require('electron')
6 |
7 | const env = 'development'
8 | const compiler = webpack(config(env))
9 |
10 | let electronStarted = false
11 |
12 | const watching = compiler.watch({}, (err, stats) => {
13 | if (!err && !stats.hasErrors() && !electronStarted) {
14 | electronStarted = true
15 |
16 | childProcess
17 | .spawn(electron, ['.'], { stdio: 'inherit' })
18 | .on('close', () => {
19 | watching.close()
20 | })
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const nodeExternals = require('webpack-node-externals')
4 |
5 | module.exports = env => {
6 | return {
7 | target: 'node',
8 | node: {
9 | __dirname: false,
10 | __filename: false
11 | },
12 | entry: {
13 | main: './src/main.js'
14 | },
15 | output: {
16 | filename: '[name].js',
17 | path: path.resolve(__dirname, './build')
18 | },
19 | externals: [ nodeExternals() ],
20 | resolve: {
21 | alias: {
22 | }
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.js$/,
28 | exclude: /node_modules/,
29 | use: ['babel-loader']
30 | }
31 | ]
32 | },
33 | plugins: [
34 | new webpack.DefinePlugin({
35 | 'NODE_ENV': JSON.stringify(env)
36 | })
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
remix-app
2 |
3 |
4 |
5 | This project packages Remix, the excellent Ethereum Solidity IDE, into
6 | a dedicated desktop application that runs from your local machine. Remix-app
7 | also caches solc compiler downloads so Remix is fully usable in airplane mode.
8 |
9 |
10 |
11 | ## Install or Build your own
12 |
13 | **To install a pre-built image, see:** https://github.com/horizon-games/remix-app/releases
14 |
15 | **To build your own:**
16 | 1. Clone this repo
17 | 2. `yarn install`
18 | 3. `yarn dist`
19 | 4. Find the distribution for your os in dist/
20 |
21 |
22 | ## Related links
23 |
24 | * Remix online version: https://remix.ethereum.org
25 | * Remix runtime: https://github.com/ethereum/remix
26 | * Remix IDE: https://github.com/ethereum/browser-solidity
27 |
28 |
29 | ## Credits
30 |
31 | * Thanks to the amazing efforts by @yann300 + team for writing https://github.com/ethereum/remix
32 | * Thanks to @acrylix for contributing
33 |
34 |
35 | ## LICENSE
36 |
37 | MIT
38 |
39 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import {} }:
2 |
3 | # a simple x11 env
4 | (pkgs.buildFHSUserEnv {
5 | name = "fhs";
6 | targetPkgs = pkgs: with pkgs; [
7 | coreutils
8 | alsaLib
9 | atk
10 | at-spi2-atk
11 | at-spi2-core
12 | cairo
13 | cups
14 | dbus
15 | expat
16 | file
17 | fontconfig
18 | freetype
19 | gdb
20 | gdk_pixbuf
21 | git
22 | glib
23 | glibc
24 | gtk2
25 | gtk3
26 | libarchive
27 | libnotify
28 | libxml2
29 | libxslt
30 | libuuid
31 | mesa_glu
32 | pango
33 | rxvt_unicode.terminfo
34 | curl
35 | openal
36 | openssl_1_0_2
37 | netcat
38 | nspr
39 | nss
40 | strace
41 | udev
42 | watch
43 | wget
44 | which
45 | xorg.libXxf86vm
46 | xorg.libX11
47 | xorg.libXScrnSaver
48 | xorg.libXcomposite
49 | xorg.libXcursor
50 | xorg.libXdamage
51 | xorg.libXext
52 | xorg.libXfixes
53 | xorg.libXi
54 | xorg.libXrandr
55 | xorg.libXrender
56 | xorg.libXtst
57 | xorg.libxcb
58 | xorg.xcbutilkeysyms
59 | zlib
60 | zsh
61 | ];
62 | runScript = "bash";
63 | }).env
64 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import url from 'url'
3 | import { app, Menu } from 'electron'
4 | import createWindow from './helpers/window'
5 | import { buildMenu } from './helpers/menu'
6 | import { registerAppCache } from './helpers/app_cache'
7 |
8 | // This line will just execute the listeners for the saving file process
9 | // import './helpers/file-manager'
10 |
11 | let userDataPath = app.getPath('userData')
12 | if (NODE_ENV !== 'production') {
13 | userDataPath = `${userDataPath} (${NODE_ENV})`
14 | app.setPath('userData', userDataPath)
15 | }
16 |
17 | if (process.platform === 'darwin') {
18 | app.setAboutPanelOptions({
19 | applicationName: 'remix-app',
20 | credits: 'Horizon Blockchain Games \n https://horizongames.net',
21 | copyright: 'Remix by https://github.com/ethereum/remix'
22 | })
23 | }
24 |
25 | app.on('ready', () => {
26 | const mainWindow = createWindow('main', {
27 | width: 1024
28 | })
29 |
30 | Menu.setApplicationMenu(Menu.buildFromTemplate(buildMenu(mainWindow)))
31 |
32 | // Register app cache on the main window's network stack
33 | registerAppCache(mainWindow, userDataPath)
34 |
35 | // Load the local static app
36 | mainWindow.loadURL(
37 | url.format({
38 | pathname: path.join(__dirname, 'app/index.html'),
39 | protocol: 'file:',
40 | slashes: true
41 | })
42 | )
43 |
44 | // Work-around for electron/chrome 51+ onbeforeunload behavior
45 | // which prevents the app window to close if not invalidated.
46 | mainWindow.webContents.on('dom-ready', () => {
47 | mainWindow.webContents.executeJavaScript("window.onbeforeunload = null")
48 | })
49 | })
50 |
51 | app.on('window-all-closed', () => {
52 | app.quit()
53 | })
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "remix",
3 | "version": "0.4.0",
4 | "description": "Remix Solidity IDE https://remix.ethereum.org - Electron Edition",
5 | "author": "Peter Kieltyka ",
6 | "credits": "Horizon Blockchain Games https://horizongames.net",
7 | "copyright": "Remix by https://github.com/ethereum/remix",
8 | "license": "MIT",
9 | "homepage": "https://github.com/horizon-games/remix-app",
10 | "main": "build/main.js",
11 | "build": {
12 | "appId": "net.horizongames.remix",
13 | "files": [
14 | "build/**/*",
15 | "resources/**"
16 | ],
17 | "directories": {
18 | "buildResources": "resources"
19 | },
20 | "win": {
21 | "target": [{
22 | "target": "portable",
23 | "arch": ["x64", "ia32"]
24 | }]
25 | },
26 | "mac": {
27 | "target": ["dmg"]
28 | },
29 | "linux": {
30 | "target": ["tar.gz"]
31 | }
32 | },
33 | "scripts": {
34 | "postinstall": "node scripts/remix-dl.js",
35 | "dev": "node scripts/start.js",
36 | "clean": "rimraf dist",
37 | "build": "webpack --mode production --color --config=webpack.config.js --env=production",
38 | "pack": "yarn dist:all --dir",
39 | "dist": "yarn build && electron-builder",
40 | "dist:all": "yarn dist -wml",
41 | "dist:windows": "yarn dist -w",
42 | "dist:mac": "yarn dist -m",
43 | "dist:linux": "yarn dist -l"
44 | },
45 | "dependencies": {
46 | "electron-fetch": "1.3.0",
47 | "fs-jetpack": "2.2.2"
48 | },
49 | "devDependencies": {
50 | "@babel/core": "7.5.4",
51 | "@babel/plugin-proposal-object-rest-spread": "7.5.4",
52 | "@babel/preset-env": "7.5.4",
53 | "babel-loader": "8.0.6",
54 | "electron": "5.0.6",
55 | "electron-builder": "20.44.4",
56 | "request": "^2.88.0",
57 | "rimraf": "2.6.3",
58 | "source-map-support": "^0.5.12",
59 | "unzipper": "0.10.1",
60 | "webpack": "4.35.3",
61 | "webpack-cli": "3.3.5",
62 | "webpack-node-externals": "1.7.2"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/helpers/app_cache.js:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, webContents } from 'electron'
2 | import fs from 'fs'
3 | import path from 'path'
4 | import jetpack from 'fs-jetpack'
5 | import fetch from 'electron-fetch'
6 |
7 | const registerAppCache = (browserWindow, userDataPath) => {
8 |
9 | // Attempt to fetch the list.json to determine if we're online
10 | // and the contents are available, in this case, we will always
11 | // fetch the latest list of available compilers, but fallback
12 | // to a local version if available.
13 | let getList = false
14 | const listURL = 'https://solc-bin.ethereum.org/bin/list.json'
15 | const resp = fetch(listURL)
16 | resp.then((resp) => {
17 | if (resp.status === 200) {
18 | getList = true
19 | }
20 | }).catch((err) => {
21 | getList = false
22 | })
23 |
24 | const filter = {
25 | urls: ['https://solc-bin.ethereum.org/bin/*']
26 | }
27 |
28 | const session = browserWindow.webContents.session
29 |
30 | session.webRequest.onBeforeRequest(filter, (details, callback) => {
31 | const resourceURL = details.url
32 | const resourceFilename = getFilename(resourceURL)
33 | const localFile = cachedFilePath(userDataPath, resourceURL)
34 |
35 | // Always fetch the new list.json if we're online
36 | if (getList && resourceFilename === 'list.json') {
37 | callback({ cancel: false })
38 | return
39 | }
40 |
41 | // Serve locally cached files if they are available
42 | if (fs.existsSync(localFile)) {
43 | callback({
44 | cancel: false,
45 | redirectURL: `file://${localFile}`
46 | })
47 | } else {
48 | callback({ cancel: false })
49 | }
50 | })
51 |
52 | session.webRequest.onCompleted(filter, (details) => {
53 | const resourceURL = details.url
54 |
55 | const resp = fetch(resourceURL)
56 |
57 | resp.then((resp) => {
58 | if (resp.status === 200) {
59 | const filename = getFilename(resourceURL)
60 | const dest = fs.createWriteStream(cachedFilePath(userDataPath, resourceURL))
61 | resp.body.pipe(dest)
62 | }
63 | }).catch((err) => {})
64 | })
65 |
66 | }
67 |
68 | const getFilename = (url) => {
69 | return url.split('/').pop()
70 | }
71 |
72 | const cachedFilePath = (userDataPath, url) => {
73 | return path.join(userDataPath, getFilename(url))
74 | }
75 |
76 | export { registerAppCache }
77 |
--------------------------------------------------------------------------------
/src/helpers/window.js:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, screen } from 'electron'
2 | import jetpack from 'fs-jetpack'
3 |
4 | export default (name, options) => {
5 | const userDataDir = jetpack.cwd(app.getPath('userData'))
6 | const stateStoreFile = `window-state-${name}.json`
7 | const defaultSize = {
8 | width: options.width,
9 | height: options.height
10 | }
11 | let state = {}
12 | let win
13 |
14 | const restore = () => {
15 | let restoredState = {}
16 | try {
17 | restoredState = userDataDir.read(stateStoreFile, 'json')
18 | } catch (err) {
19 | // For some reason json can't be read (might be corrupted).
20 | // No worries, we have defaults.
21 | }
22 | return Object.assign({}, defaultSize, restoredState)
23 | }
24 |
25 | const getCurrentPosition = () => {
26 | const position = win.getPosition()
27 | const size = win.getSize()
28 | return {
29 | x: position[0],
30 | y: position[1],
31 | width: size[0],
32 | height: size[1]
33 | }
34 | }
35 |
36 | const windowWithinBounds = (windowState, bounds) => {
37 | return (
38 | windowState.x >= bounds.x &&
39 | windowState.y >= bounds.y &&
40 | windowState.x + windowState.width <= bounds.x + bounds.width &&
41 | windowState.y + windowState.height <= bounds.y + bounds.height
42 | )
43 | }
44 |
45 | const resetToDefaults = () => {
46 | const bounds = screen.getPrimaryDisplay().bounds;
47 | return Object.assign({}, defaultSize, {
48 | x: (bounds.width - defaultSize.width) / 2,
49 | y: (bounds.height - defaultSize.height) / 2
50 | })
51 | }
52 |
53 | const ensureVisibleOnSomeDisplay = windowState => {
54 | const visible = screen.getAllDisplays().some(display => {
55 | return windowWithinBounds(windowState, display.bounds);
56 | })
57 | if (!visible) {
58 | // Window is partially or fully not visible now.
59 | // Reset it to safe defaults.
60 | return resetToDefaults()
61 | }
62 | return windowState
63 | }
64 |
65 | const saveState = () => {
66 | if (!win.isMinimized() && !win.isMaximized()) {
67 | Object.assign(state, getCurrentPosition())
68 | }
69 | userDataDir.write(stateStoreFile, state, { atomic: true })
70 | }
71 |
72 | state = ensureVisibleOnSomeDisplay(restore())
73 |
74 | win = new BrowserWindow(Object.assign({}, options, state))
75 |
76 | win.on('close', saveState)
77 |
78 | return win
79 | }
80 |
--------------------------------------------------------------------------------
/scripts/remix-dl.js:
--------------------------------------------------------------------------------
1 | // remix-dl script downloads remix distribution to be packaged in
2 | // the electron app.
3 | const os = require('os')
4 | const request = require('request')
5 | const fs = require('fs')
6 | const path = require('path')
7 | const unzipper = require('unzipper')
8 | const rimraf = require('rimraf')
9 |
10 | const fetchLatestRemixDownloadURL = (cb) => {
11 | const repoListURL = 'https://api.github.com/repos/ethereum/remix-live/contents/?ref=gh-pages'
12 | const githubReq = {
13 | url: repoListURL,
14 | headers: {
15 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.88 Safari/537.36'
16 | }
17 | }
18 |
19 | request.get(githubReq, (err, resp, body) => {
20 | if (err) {
21 | console.log('github error:', err)
22 | process.exit(1)
23 | }
24 |
25 | if (resp.statusCode !== 200) {
26 | console.log('github: failed to fetch remix download url, got status', resp.statusCode)
27 | process.exit(1)
28 | }
29 |
30 | const repo = JSON.parse(body)
31 | let downloadURL
32 |
33 | repo.forEach((obj) => {
34 | if (obj.name.match(/remix-[a-z0-9]+\.zip/i)) {
35 | downloadURL = obj.download_url
36 | }
37 | })
38 |
39 | if (downloadURL) {
40 | cb(downloadURL)
41 | } else {
42 | console.log('github: failed to get latest remix download url')
43 | process.exit(1)
44 | }
45 | })
46 | }
47 |
48 | const fetchRemixApp = (url, destPath) => {
49 | const ret = () => {
50 | const appDir = path.join(path.dirname(fs.realpathSync(__filename)), '../build/app')
51 |
52 | rimraf(appDir, () => {
53 | const reader = fs.createReadStream(destPath)
54 | reader.pipe(unzipper.Extract({ path: appDir }))
55 | fs.unlinkSync(destPath)
56 | })
57 | }
58 |
59 | const err = (msg) => {
60 | console.error(msg)
61 | }
62 |
63 | const file = fs.createWriteStream(destPath)
64 | const sendReq = request.get(url)
65 |
66 | sendReq.on('response', (response) => {
67 | if (response.statusCode !== 200) {
68 | return err('Unable to download remix file, status: ' + response.statusCode)
69 | }
70 | })
71 |
72 | sendReq.on('error', (err) => {
73 | fs.unlink(destPath)
74 | return err(err.message)
75 | })
76 |
77 | sendReq.pipe(file)
78 |
79 | file.on('finish', () => {
80 | file.close(ret)
81 | })
82 |
83 | file.on('error', (err) => {
84 | fs.unlink(destPath)
85 | return err(err.message)
86 | })
87 | }
88 |
89 |
90 |
91 | fetchLatestRemixDownloadURL((downloadURL) => {
92 | console.log(`latest remix download url at ${downloadURL}, downloading & unpacking..`)
93 |
94 | const destPath = path.join(os.tmpdir(), 'remix.zip')
95 | if (fs.existsSync(destPath)) {
96 | fs.unlinkSync(destPath)
97 | }
98 | fetchRemixApp(downloadURL, destPath)
99 | })
100 |
--------------------------------------------------------------------------------
/src/helpers/menu.js:
--------------------------------------------------------------------------------
1 | import { app, shell, BrowserWindow, BrowserView, dialog } from 'electron'
2 |
3 | const pkgJson = require('../../package.json')
4 |
5 | function buildMenu(mainWindow) {
6 | let menu = []
7 |
8 |
9 | if (process.platform === 'darwin') {
10 | menu.push({
11 | label: app.getName(),
12 | submenu: [
13 | { role: 'about' },
14 | { type: 'separator' },
15 | { role: 'services', submenu: [] },
16 | { type: 'separator' },
17 | { role: 'hide' },
18 | { role: 'hideothers' },
19 | { role: 'unhide' },
20 | { type: 'separator' },
21 | { role: 'quit' }
22 | ]
23 | })
24 | } else {
25 | menu.push({
26 | label: 'File',
27 | submenu: [
28 | { role: 'quit' }
29 | ]
30 | })
31 | }
32 |
33 | menu.push(...[
34 | {
35 | label: 'Edit',
36 | submenu: [
37 | { role: 'undo' },
38 | { role: 'redo' },
39 | { type: 'separator' },
40 | {
41 | label: 'Save as...',
42 | accelerator: 'Shift+CmdOrCtrl+S',
43 | click: () => {
44 | const win = BrowserWindow.getFocusedWindow()
45 | // The data of the file will be always in localStorage and
46 | // we can get the title of the file by getting the active tab text
47 | win.webContents.executeJavaScript(`
48 | ((window) => {
49 | try {
50 | const title = window.document.getElementsByClassName('active')[0].childNodes[4].title;
51 | if(title === "Home" || !title) {
52 | alert('No file opened')
53 | } else {
54 | require('electron').ipcRenderer.send('save-file', {title, content: window.localStorage.getItem('sol:' + title)})
55 | }
56 | } catch(err){
57 | console.log(err)
58 | alert('No file opened')
59 | }
60 | })(window)
61 | `);
62 | }
63 | },
64 | { type: 'separator' },
65 | { role: 'cut' },
66 | { role: 'copy' },
67 | { role: 'paste' },
68 | { role: 'selectall' }
69 | ]
70 | },
71 | {
72 | role: 'window',
73 | submenu: [
74 | {
75 | label: 'Reload',
76 | accelerator: 'CmdOrCtrl+R',
77 | click: () => {
78 | const win = BrowserWindow.getFocusedWindow()
79 | dialog.showMessageBox(
80 | win,
81 | {
82 | type: 'warning',
83 | buttons: ['Cancel', 'Ok'],
84 | message: 'Do you want to reload the app?',
85 | detail: 'Changes that you made may not be saved'
86 | },
87 | id => {
88 | // 1 is the index of Ok button, which is the confirmation of reload
89 | if(id === 1) {
90 | win.reload()
91 | }
92 | }
93 | );
94 | }
95 | },
96 | { role: 'forcereload' },
97 | { role: 'minimize' },
98 | {
99 | label: 'Close',
100 | accelerator: 'Cmd+W',
101 | click: () => {
102 | const win = BrowserWindow.getFocusedWindow()
103 | if (win === null) {
104 | if (mainWindow.isDevToolsFocused()) {
105 | mainWindow.closeDevTools()
106 | }
107 | } else {
108 | if (process.platform === 'darwin') {
109 | app.hide()
110 | } else {
111 | win.close()
112 | }
113 | }
114 | }
115 | }
116 | ]
117 | },
118 | {
119 | label: 'Inspector',
120 | submenu: [
121 | {
122 | label: 'Toggle DevTools',
123 | accelerator: 'Alt+CmdOrCtrl+I',
124 | click: () => {
125 | BrowserWindow.getFocusedWindow().toggleDevTools();
126 | }
127 | }
128 | ]
129 | },
130 | {
131 | role: 'help',
132 | submenu: [
133 | {
134 | label: 'Learn More',
135 | click: () => {
136 | shell.openExternal(pkgJson.homepage)
137 | }
138 | }
139 | ]
140 | }
141 | ])
142 |
143 | return menu
144 | }
145 |
146 | export { buildMenu }
147 |
--------------------------------------------------------------------------------