├── .babelrc ├── .doolrc ├── .eslintrc ├── .gitignore ├── README.md ├── build ├── clean.js ├── index.js └── main.js ├── images └── image.png ├── package.json └── src ├── app.html ├── components ├── App.js ├── Header.js ├── List.js ├── Mount.js └── NoData.js ├── icons ├── app.icns ├── icon.less ├── iconfont.eot ├── iconfont.svg ├── iconfont.ttf └── iconfont.woff ├── index.js ├── index.less ├── main.js └── menu.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ['env', 'stage-2', 'react'], 3 | "plugins": ["transform-runtime"] 4 | } -------------------------------------------------------------------------------- /.doolrc: -------------------------------------------------------------------------------- 1 | { 2 | babelPlugins: ['transform-runtime'] 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | parser: babel-eslint 2 | extends: airbnb 3 | 4 | env: 5 | browser: true 6 | 7 | rules: 8 | camelcase: 0 9 | comma-dangle: 0 10 | semi: [2, always] 11 | no-underscore-dangle: 0 12 | object-curly-spacing: [2, always] 13 | import/no-extraneous-dependencies: 0 14 | react/no-danger: 0 15 | react/forbid-prop-types: 0 16 | react/jsx-filename-extension: 0 17 | jsx-a11y/href-no-hash: 0 18 | jsx-a11y/no-static-element-interactions: 0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | .ipr 4 | .iws 5 | *~ 6 | ~* 7 | *.diff 8 | *.patch 9 | *.bak 10 | .DS_Store 11 | Thumbs.db 12 | .project 13 | .*proj 14 | .svn/ 15 | *.swp 16 | *.swo 17 | *.log 18 | *.sublime-project 19 | *.sublime-workspace 20 | 21 | npm-debug.log 22 | package-lock.json 23 | node_modules 24 | 25 | .buildpath 26 | .settings 27 | coverage 28 | .nyc_output 29 | 30 | app/ 31 | dist/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Disky 2 | ===== 3 | 4 | > Make NTFS writable on macOS 5 | 6 | ![Disky](images/image.png) 7 | 8 | ## Development 9 | 10 | ```bash 11 | npm i dool -g 12 | npm i 13 | 14 | npm run dev 15 | npm run start 16 | 17 | # Electron Mirror of China 18 | ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/" 19 | 20 | npm run pack 21 | npm run disk 22 | ``` 23 | 24 | ## Report a issue 25 | 26 | * [All issues](https://github.com/d-band/disky/issues) 27 | * [New issue](https://github.com/d-band/disky/issues/new) 28 | 29 | ## License 30 | 31 | Disky is available under the terms of the MIT License. 32 | -------------------------------------------------------------------------------- /build/clean.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const join = require('path').join; 3 | const rimraf = require('rimraf'); 4 | 5 | const dist = join(__dirname, '../dist'); 6 | rimraf(dist, () => { 7 | fs.mkdirSync(dist); 8 | const html = fs.readFileSync( 9 | join(__dirname, '../src/app.html'), 10 | 'utf8' 11 | ); 12 | fs.writeFileSync( 13 | join(dist, 'app.html'), 14 | html.replace(/http:\/\/localhost:8000/g, '.'), 15 | 'utf8' 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (config) => { 2 | config.target = 'electron-renderer'; 3 | config.entry = { 4 | index: './src/index.js' 5 | }; 6 | return config; 7 | } -------------------------------------------------------------------------------- /build/main.js: -------------------------------------------------------------------------------- 1 | module.exports = (config) => { 2 | config.target = 'electron-main'; 3 | config.entry = { 4 | main: './src/main.js' 5 | }; 6 | config.node = { 7 | __dirname: false, 8 | __filename: false 9 | }; 10 | return config; 11 | } -------------------------------------------------------------------------------- /images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-band/disky/77e4197147492a03ac2c6c9087fdbadcdd967680/images/image.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "disky", 3 | "version": "1.0.0", 4 | "description": "NTFS writable for macOS", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "lint": "eslint --ext .js src", 8 | "dev": "dool server --config build/index.js", 9 | "start": "NODE_ENV=dev electron -r babel-register ./src/main.js", 10 | "build-main": "dool build --no-compress --config build/main.js", 11 | "build-index": "dool build --config build/index.js", 12 | "prebuild": "node build/clean.js", 13 | "build": "npm run build-index && npm run build-main", 14 | "pack": "npm run build && rimraf app && electron-builder --dir", 15 | "dist": "npm run build && rimraf app && electron-builder" 16 | }, 17 | "build": { 18 | "appId": "com.dband.disky", 19 | "productName": "Disky", 20 | "directories": { 21 | "output": "app" 22 | }, 23 | "files": [ 24 | "dist/**/*", 25 | "app.html" 26 | ], 27 | "mac": { 28 | "category": "public.app-category.developer-tools", 29 | "icon": "src/icons/app.icns" 30 | } 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/d-band/disky.git" 35 | }, 36 | "keywords": [ 37 | "NTFS", 38 | "macOS" 39 | ], 40 | "author": "d-band", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/d-band/disky/issues" 44 | }, 45 | "homepage": "https://github.com/d-band/disky#readme", 46 | "devDependencies": { 47 | "babel-eslint": "^8.0.0", 48 | "babel-plugin-transform-runtime": "^6.23.0", 49 | "babel-preset-env": "^1.6.0", 50 | "babel-preset-react": "^6.24.1", 51 | "babel-preset-stage-2": "^6.24.1", 52 | "babel-register": "^6.26.0", 53 | "electron": "^1.7.6", 54 | "electron-builder": "^19.27.7", 55 | "eslint": "^4.7.1", 56 | "eslint-config-airbnb": "^15.1.0", 57 | "eslint-plugin-import": "^2.7.0", 58 | "eslint-plugin-jsx-a11y": "^5.1.1", 59 | "eslint-plugin-react": "^7.3.0", 60 | "rimraf": "^2.6.2" 61 | }, 62 | "dependencies": { 63 | "ls-usb": "0.1.0", 64 | "prop-types": "^15.5.10", 65 | "react": "^15.6.1", 66 | "react-dom": "^15.6.1", 67 | "react-redux": "^5.0.6", 68 | "sudo-prompt": "^7.1.1", 69 | "yax": "^0.3.1" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Disky 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { mapState, mapActions } from 'yax'; 5 | import List from './List'; 6 | import Mount from './Mount'; 7 | 8 | class App extends Component { 9 | static propTypes = { 10 | mode: PropTypes.string.isRequired, 11 | page: PropTypes.string.isRequired, 12 | getMedias: PropTypes.func.isRequired 13 | }; 14 | componentWillMount() { 15 | this.props.getMedias(); 16 | } 17 | render() { 18 | const { mode, page } = this.props; 19 | return ( 20 |
21 | {page === 'list' ? : } 22 |
23 | ); 24 | } 25 | } 26 | 27 | export default connect( 28 | mapState({ mode: 'mode', page: 'page' }), 29 | mapActions({ getMedias: 'getMedias' }) 30 | )(App); 31 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { ipcRenderer, remote } from 'electron'; 5 | 6 | function Header({ mode, dispatch }) { 7 | const refresh = () => dispatch({ 8 | type: 'getMedias' 9 | }); 10 | const openMenu = () => { 11 | ipcRenderer.send('open-menu', mode); 12 | }; 13 | const hide = () => { 14 | remote.app.hide(); 15 | }; 16 | return ( 17 |
18 |
19 | 20 |
21 |
Disky
22 |
23 | 24 | 25 |
26 |
27 | ); 28 | } 29 | 30 | Header.propTypes = { 31 | mode: PropTypes.string.isRequired, 32 | dispatch: PropTypes.func.isRequired 33 | }; 34 | 35 | export default connect(({ mode }) => ({ mode }))(Header); 36 | -------------------------------------------------------------------------------- /src/components/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { shell } from 'electron'; 5 | import { mapState } from 'yax'; 6 | import Header from './Header'; 7 | import NoData from './NoData'; 8 | 9 | function List({ list, dispatch }) { 10 | const eject = (v) => { 11 | if (!v.mount) return; 12 | dispatch({ type: 'eject', payload: v.node }); 13 | }; 14 | const open = (v) => { 15 | if (!v.mount) return; 16 | shell.openExternal(`file://${v.mount}`); 17 | }; 18 | return ( 19 |
20 |
21 |
22 | {list.length ? null : } 23 | {list.map(media => ( 24 |
25 |
{media.name}
26 | {media.volumes.map((v) => { 27 | const percent = 1 - ((v.free_bytes || 0) / v.size_bytes); 28 | const width = `${Math.floor(percent * 100)}%`; 29 | const meta = v.free ? `${v.free} free of ${v.size}` : v.size; 30 | return ( 31 |
32 |
open(v)}> 33 | 34 |
35 |
open(v)}> 36 |
{v.name}
37 |
38 |
39 |
40 |
{meta}
41 |
42 |
eject(v)}> 43 | 44 |
45 |
46 | ); 47 | })} 48 |
49 | ))} 50 |
51 |
52 | ); 53 | } 54 | 55 | List.propTypes = { 56 | list: PropTypes.array, 57 | dispatch: PropTypes.func.isRequired 58 | }; 59 | List.defaultProps = { 60 | list: [] 61 | }; 62 | 63 | export default connect( 64 | mapState({ list: 'list' }) 65 | )(List); 66 | -------------------------------------------------------------------------------- /src/components/Mount.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | function Mount({ dispatch }) { 6 | const gotoList = () => dispatch({ 7 | type: 'gotoPage', 8 | payload: 'list' 9 | }); 10 | const mount = () => dispatch({ 11 | type: 'mount' 12 | }); 13 | return ( 14 |
15 | 16 | 17 | 18 | 19 | 20 | Activate 21 | 22 |
23 | ); 24 | } 25 | 26 | Mount.propTypes = { 27 | dispatch: PropTypes.func.isRequired 28 | }; 29 | 30 | export default connect()(Mount); 31 | -------------------------------------------------------------------------------- /src/components/NoData.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | function NoData({ dispatch }) { 6 | const refresh = () => dispatch({ 7 | type: 'getMedias' 8 | }); 9 | return ( 10 |
11 |

No flash disk

12 |

Refresh by clicking below button

13 |

14 | 15 | 16 | 17 |

18 |
19 | ); 20 | } 21 | 22 | NoData.propTypes = { 23 | dispatch: PropTypes.func.isRequired 24 | }; 25 | 26 | export default connect()(NoData); 27 | -------------------------------------------------------------------------------- /src/icons/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-band/disky/77e4197147492a03ac2c6c9087fdbadcdd967680/src/icons/app.icns -------------------------------------------------------------------------------- /src/icons/icon.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; 3 | src: url('iconfont.eot?t=1505517923011'); /* IE9*/ 4 | src: url('iconfont.eot?t=1505517923011#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'), url('iconfont.ttf?t=1505517923011') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 6 | url('iconfont.svg?t=1505517923011#iconfont') format('svg'); /* iOS 4.1- */ 7 | } 8 | .icon { 9 | font-family: "iconfont" !important; 10 | font-style: normal; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | .icon-close:before { 15 | content: "\e627"; 16 | } 17 | .icon-refresh:before { 18 | content: "\e68a"; 19 | } 20 | .icon-hardisk:before { 21 | content: "\e68d"; 22 | } 23 | .icon-more:before { 24 | content: "\e609"; 25 | } 26 | .icon-eject:before { 27 | content: "\e642"; 28 | } 29 | .icon-rocket:before { 30 | content: "\e505"; 31 | } -------------------------------------------------------------------------------- /src/icons/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-band/disky/77e4197147492a03ac2c6c9087fdbadcdd967680/src/icons/iconfont.eot -------------------------------------------------------------------------------- /src/icons/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/icons/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-band/disky/77e4197147492a03ac2c6c9087fdbadcdd967680/src/icons/iconfont.ttf -------------------------------------------------------------------------------- /src/icons/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-band/disky/77e4197147492a03ac2c6c9087fdbadcdd967680/src/icons/iconfont.woff -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { ipcRenderer } from 'electron'; 5 | import yax from 'yax'; 6 | import App from './components/App'; 7 | import './index.less'; 8 | 9 | const store = yax({ 10 | state: { 11 | mode: 'dark', 12 | page: 'list', 13 | list: [] 14 | }, 15 | reducers: { 16 | gotoPage(state, page) { 17 | return { ...state, page }; 18 | }, 19 | getMediasDone(state, list) { 20 | let page = 'list'; 21 | // If has read-only goto mount page 22 | list.forEach((device) => { 23 | device.volumes.forEach((item) => { 24 | if (!item.writable) { 25 | page = 'mount'; 26 | } 27 | }); 28 | }); 29 | return { ...state, list, page }; 30 | }, 31 | toggleMode(state) { 32 | const { mode } = state; 33 | return { ...state, mode: mode === 'dark' ? 'light' : 'dark' }; 34 | } 35 | }, 36 | actions: { 37 | mount() { 38 | ipcRenderer.send('mount-ntfs'); 39 | }, 40 | eject(ctx, data) { 41 | ipcRenderer.send('eject', data); 42 | }, 43 | getMedias() { 44 | ipcRenderer.send('get-medias'); 45 | } 46 | } 47 | }); 48 | 49 | ipcRenderer.on('mount-done', () => { 50 | ipcRenderer.send('get-medias'); 51 | }); 52 | ipcRenderer.on('get-medias-done', (event, data) => { 53 | store.dispatch({ 54 | type: 'getMediasDone', 55 | payload: data 56 | }); 57 | }); 58 | ipcRenderer.on('toggle-mode', () => { 59 | store.dispatch({ 60 | type: 'toggleMode' 61 | }); 62 | }); 63 | 64 | render( 65 | 66 | 67 | , 68 | document.getElementById('root') 69 | ); 70 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | @import './icons/icon.less'; 2 | 3 | body { 4 | font-size: 14px; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif; 6 | } 7 | * { 8 | box-sizing: border-box; 9 | border: none; 10 | margin: 0; 11 | padding: 0; 12 | outline: none !important; 13 | } 14 | a { 15 | cursor: pointer; 16 | 17 | &:hover { 18 | opacity: 0.7; 19 | } 20 | &:active { 21 | opacity: 1; 22 | } 23 | } 24 | .dark { 25 | color: #fff; 26 | background-color: #13161f; 27 | 28 | .header { 29 | background-color: #1e1e2a; 30 | } 31 | .main .list { 32 | dt { 33 | color: #7f818e; 34 | } 35 | dd { 36 | background-color: #1e1e2a; 37 | } 38 | } 39 | } 40 | .light { 41 | color: #333; 42 | background-color: #f1f1f1; 43 | 44 | .btn { 45 | color: #3777cf; 46 | } 47 | .header { 48 | background-color: #fff; 49 | } 50 | .main .list { 51 | dt { 52 | color: #888; 53 | } 54 | dd { 55 | background-color: #fff; 56 | } 57 | } 58 | } 59 | .mount-page, .list-page { 60 | height: 100vh; 61 | display: flex; 62 | flex-direction: column; 63 | } 64 | .mount-page { 65 | position: relative; 66 | align-items: center; 67 | justify-content: center; 68 | 69 | .btn-close { 70 | position: absolute; 71 | top: 10px; 72 | right: 10px; 73 | font-size: 24px; 74 | } 75 | .btn-activate { 76 | width: 100px; 77 | height: 100px; 78 | border-radius: 50%; 79 | display: flex; 80 | flex-direction: column; 81 | align-items: center; 82 | justify-content: center; 83 | color: #fff; 84 | background-color: #3777cf; 85 | 86 | i { 87 | font-size: 32px; 88 | } 89 | } 90 | } 91 | .no-data { 92 | margin-top: 50%; 93 | text-align: center; 94 | line-height: 1.7; 95 | 96 | .btn-refresh { 97 | font-size: 40px; 98 | } 99 | } 100 | .header { 101 | padding: 10px; 102 | display: flex; 103 | align-items: center; 104 | justify-content: center; 105 | -webkit-user-select: none; 106 | -webkit-app-region: drag; 107 | 108 | .left, .right { 109 | width: 20%; 110 | } 111 | .right { 112 | text-align: right; 113 | line-height: 1; 114 | font-size: 20px; 115 | 116 | .btn { 117 | margin-left: 10px; 118 | } 119 | } 120 | .left { 121 | display: flex; 122 | align-items: center; 123 | 124 | .btn-close { 125 | display: inline-block; 126 | color: #fc605c; 127 | background-color: #fc605c; 128 | width: 12px; 129 | height: 12px; 130 | font-size: 8px; 131 | text-align: center; 132 | border-radius: 50%; 133 | border: 1px solid #f34f4a; 134 | 135 | &:hover { 136 | color: #13161f; 137 | opacity: 1; 138 | } 139 | } 140 | } 141 | .title { 142 | flex: 1; 143 | font-size: 16px; 144 | text-align: center; 145 | } 146 | } 147 | .main { 148 | flex: 1; 149 | overflow-y: scroll; 150 | } 151 | .main .list { 152 | dt { 153 | padding: 3px 15px; 154 | } 155 | dd { 156 | padding: 10px; 157 | display: flex; 158 | align-items: center; 159 | cursor: pointer; 160 | 161 | &:hover { 162 | color: #fff; 163 | background-color: #3777cf; 164 | } 165 | &.disabled { 166 | color: darken(#fff, 40%); 167 | } 168 | .info { 169 | flex: 1; 170 | line-height: 1; 171 | padding: 0 10px; 172 | 173 | .title { 174 | margin-bottom: 4px; 175 | } 176 | .progress { 177 | height: 8px; 178 | margin-bottom: 4px; 179 | border-radius: 2px; 180 | background-color: #1bc98e; 181 | 182 | .percent { 183 | width: 80%; 184 | height: 8px; 185 | border-radius: 2px; 186 | background-color: #1ca8dd; 187 | } 188 | } 189 | .meta { 190 | font-size: 12px; 191 | } 192 | } 193 | .left, .right { 194 | align-items: center; 195 | } 196 | .left i { 197 | font-size: 42px; 198 | } 199 | .right i { 200 | font-size: 18px; 201 | } 202 | } 203 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain } from 'electron'; 2 | import getMediaList, { execAsync } from 'ls-usb'; 3 | import sudo from 'sudo-prompt'; 4 | import createMenu from './menu'; 5 | 6 | function sudoAsync(cmd) { 7 | return new Promise((resolve, reject) => { 8 | sudo.exec(cmd, { 9 | name: 'Disky' 10 | }, (err) => { 11 | if (err) return reject(err); 12 | return resolve(); 13 | }); 14 | }); 15 | } 16 | 17 | let win = null; 18 | app.on('ready', () => { 19 | win = new BrowserWindow({ 20 | show: false, 21 | frame: false, 22 | width: 360, 23 | height: 572, 24 | fullscreenable: false, 25 | maximizable: false, 26 | resizable: false, 27 | backgroundColor: '#13161f' 28 | }); 29 | 30 | win.once('ready-to-show', () => { 31 | win.show(); 32 | }); 33 | 34 | win.loadURL(`file://${__dirname}/app.html`); 35 | 36 | win.on('closed', () => { 37 | win = null; 38 | }); 39 | ipcMain.on('open-menu', async (event, data) => { 40 | const menu = await createMenu(app, win, data); 41 | menu.popup(); 42 | }); 43 | }); 44 | 45 | ipcMain.on('get-medias', async (event) => { 46 | const data = await getMediaList(); 47 | event.sender.send('get-medias-done', data); 48 | }); 49 | ipcMain.on('eject', async (event, arg) => { 50 | await execAsync(`diskutil eject ${arg}`); 51 | event.sender.send('mount-done'); 52 | }); 53 | ipcMain.on('mount-ntfs', async (event) => { 54 | const data = await getMediaList(); 55 | const cmd = []; 56 | let isSudo = false; 57 | data.forEach((media) => { 58 | media.volumes.forEach((item) => { 59 | const { node, mount, fs_type, writable, udid } = item; 60 | if (!node || writable) return; 61 | if (fs_type === 'ntfs') { 62 | isSudo = true; 63 | const dir = mount || `/Volumes/${udid}`; 64 | if (mount) { 65 | cmd.push(`diskutil umount ${node}`); 66 | } 67 | cmd.push(`mkdir "${dir}"`); 68 | cmd.push(`mount -o rw,auto,nobrowse -t ntfs ${node} "${dir}"`); 69 | } else { 70 | cmd.push(`diskutil mount ${node}`); 71 | } 72 | }); 73 | }); 74 | if (cmd.length) { 75 | if (isSudo) { 76 | await sudoAsync(cmd.join('&&')); 77 | } else { 78 | await execAsync(cmd.join('&&')); 79 | } 80 | event.sender.send('mount-done'); 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /src/menu.js: -------------------------------------------------------------------------------- 1 | import { Menu } from 'electron'; 2 | 3 | export default function createMenu(app, win, mode) { 4 | return Menu.buildFromTemplate([{ 5 | label: 'Dark mode', 6 | type: 'checkbox', 7 | checked: mode === 'dark', 8 | click() { 9 | win.webContents.send('toggle-mode'); 10 | } 11 | }, { 12 | type: 'separator' 13 | }, { 14 | label: 'Quit', 15 | click: app.quit, 16 | role: 'quit' 17 | }]); 18 | } 19 | --------------------------------------------------------------------------------