JonSn0w/Hyde

View on GitHub
main.js

Summary

Maintainability
B
5 hrs
Test Coverage
/*
 * HYDE - markdown editor
 * Copyright (c) 2017 Brett Stevenson <bstevensondev@gmail.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

'use strict';
// handle setupevents as quickly as possible
const setupEvents = require('./js/installer/setupEvents');
if(setupEvents.handleSquirrelEvent()) { return; }
const electron = require('electron');
const app = electron.app;
const ipc = electron.ipcMain;
const dialog = electron.dialog;
const shell = electron.shell;
const BrowserWindow = electron.BrowserWindow;
const {Menu, MenuItem} = require('electron');
const config = require('./js/config');
const tray = require('./tray');
const settings = require('electron-settings');
const fs = require('fs');
const path = require('path');
const window = require('electron-window');
const localShortcut = require('electron-localshortcut');
const windowStateManager = require('electron-window-state');
const packageJSON = require(__dirname + '/package.json');
const mainPage = path.join(__dirname, 'index.html');
const version = app.getVersion();
const args = require('yargs')
    .usage('Hyde v'+version+'\n\n Usage: hyde [options] <filename>\n\n If a filename isn\'t specified, the application will open with the most recently opened file. Additionally, if the specified file doesn\'t exist, a new file with the given filename will be created at the specified path.')
    .option('d', {
      alias: 'dev',
      describe: 'Open in development mode',
      type: 'boolean'
    })
    .alias('v', 'version')
    .version('v'+version)
    .option('gh', {
      alias: 'github',
      describe: 'Open a README from GitHub',
      type: 'string'
    })
    .help('h')
    .alias('h', 'help')
    .wrap(60)
    .argv;

// Keep a global reference of the window objects
var windows = new Set();
let isQuitting = false;
var hasChanges = false;
let windowState, mainWindow, pathToOpen;

// Allows render process to access active windows
const getWindows = exports.getWindows = () => window.windows;
const lshortcuts = exports.lshortcuts = () => localShortcut;

function getWindowConfig() {
  var conf = {
      width: windowState.width,
      height: windowState.height,
      x: windowState.x,
      y: windowState.y,
      minWidth: 400,
      minHeight: 300,
      show: false,
      frame: false,
      autoHideMenuBar: true,
      fullscreenWindowTitle: true,
      darkTheme: true,
      transparent: false
  }
  if (process.platform === 'darwin') {
    conf.titleBarStyle = 'hidden';
    conf.icon = path.join(__dirname,'assets','img','icon','icns','icon.icns');
  } else if (process.platform === 'win32') {
    conf.icon = path.join(__dirname,'assets','img','icon','ico','icon.ico');
  } else {
    conf.icon = path.join(__dirname,'assets','img','icon','png','64x64.png');
  }
  return conf;
}

// Check for file from commandline
var readFile = null;
process.argv.forEach(function(val, index, array) {
  if (index >= 2 && val.includes('.md')) {
    readFile = val;
  }
});

const createWindow = exports.createWindow = (filePath) => {
  let argFile;
  let newWindow = window.createWindow(getWindowConfig());
  if(settings.has('targetFile')) {
    argFile = { file: settings.get('targetFile') };
    window.windows[newWindow.id].filePath = { 'filename': settings.get('targetFile') };
    settings.delete('targetFile');
  }
  newWindow.showUrl(mainPage, argFile);
  windowState.manage(newWindow)
  newWindow.on('did-finish-load', () => { mainWindow.show(); });
  newWindow.on('closed', () => { newWindow = null; });

  // Listen for issues
  newWindow.on('unresponsive', (err) => { console.error(err); });
  newWindow.webContents.on('crashed', (err) => { console.error(err); });
  // Open anchor links in browser
  function openExternal (e, url) {
    e.preventDefault();
    shell.openExternal(url);
  }
  newWindow.webContents.on('new-window', openExternal)
  newWindow.webContents.on('will-navigate', openExternal)
  return newWindow
}

ipc.on('export-to-pdf', (event, pdfPath) => {
  let win = BrowserWindow.fromWebContents(event.sender);
  win.webContents.printToPDF({ 'printBackground': true, 'pageSize': 'A4' }, (error, data) => {
    if (error) throw error;
    fs.writeFile(pdfPath, data, (error) => {
      if (error) throw error;
      shell.openExternal('file://' + pdfPath);
      event.sender.send('wrote-pdf', pdfPath);
    });
  });
});
ipc.on('export-to-html', (event, data, htmlPath) => {
    fs.writeFile(htmlPath, data, function(err) {
        if(err) notify('An error ocurred while creating \''+filename+'\' '+err.message, 'error');
    });
  event.sender.send('wrote-html', htmlPath);
});

const getThemes = exports.getThemes = () => {
  return require(path.join(__dirname, 'assets','json','themes.json'));
}

function menuThemes() {
  var themes = [];
      // themeFiles = fs.readdirSync(path.join(__dirname,'assets','css','themes'));
  getThemes().forEach((theme) => {
    var temp = { label:theme.name, click:() => { ipcSend('set-theme', theme.value); settings.set('editorTheme', theme.value.toString()); }};
    themes.push(temp);
  });
  return themes;
}

var template = [
  {label:'&File', submenu: [
        {label:'New', accelerator:'CmdOrCtrl+N', click:() => { createWindow(); }},
        {label:'Open...', accelerator:'CmdOrCtrl+O', click:() => { ipcSend('file-open'); }},
    {label:'Open in New Window', accelerator:'CmdOrCtrl+Shift+O', click:() => { ipcSend('file-open-new'); }},
        {type:'separator'},
        {label:'Save', accelerator:'CmdOrCtrl+S', click:() => { ipcSend('file-save'); }},
        {label:'Save As...', accelerator:'CmdOrCtrl+Shift+S', click:() => { ipcSend('file-save-as'); }},
        {label:'Export to PDF', click:() => { ipcSend('file-pdf'); }},
    {label:'Export to HTML', click:() => { ipcSend('file-html'); }},
        {type:'separator'},
        {label:'Show in File Manager', click:() => { ipcSend('open-file-manager'); }},
        {type:'separator'},
        {label:'Settings', accelerator:'CmdOrCtrl+,', click:() => { ipcSend('toggle-settings'); }},
        {type:'separator'},
        {label:'Quit', accelerator:'CmdOrCtrl+Q', click:() => { ipcSend('win-close'); }}
    ]},
    {label:'&Edit', submenu: [
        {label:'Undo', accelerator:'CmdOrCtrl+Z', role: 'undo'},
        {label:'Redo', accelerator:'CmdOrCtrl+Shift+Z', role: 'redo'},
        {type:'separator'},
        {label:'Cut', accelerator:'CmdOrCtrl+X', role: 'cut'},
        {label:'Copy', accelerator:'CmdOrCtrl+C', role: 'copy'},
        {label:'Paste', accelerator:'CmdOrCtrl+V', role: 'paste'},
        {label:'Select All', accelerator:'CmdOrCtrl+A', click:() => { ipcSend('select-all'); }},
        {type:'separator'},
        {label:'Find', accelerator:'CmdOrCtrl+F', click:() => { ipcSend('search-find'); }},
        {label:'Replace', accelerator:'CmdOrCtrl+Shift+F', click:() => { ipcSend('search-replace'); }},
        {type:'separator'},
        {label:'Auto-Indent', accelerator:'CmdOrCtrl+Shift+A', click:() => { ipcSend('auto-indent'); }},
        {label:'Indent Less', accelerator:'CmdOrCtrl+Left', click:() => { ipcSend('indent-less'); }},
        {label:'Indent More', accelerator:'CmdOrCtrl+Right', click:() => { ipcSend('indent-more'); }},
        {type:'separator'},
    ]},
    {label:'&View', submenu: [
        {label:'Reload', accelerator:'CmdOrCtrl+R', click:() => { ipcSend('win-reload'); }},
        {type:'separator'},
        {label:'Toggle Menu', accelerator:'CmdOrCtrl+M', click:() => { ipcSend('toggle-menu'); }},
        {label:'Toggle Toolbar', accelerator:'CmdOrCtrl+.', click:() => { ipcSend('toggle-toolbar'); }},
        {label:'Toggle Preview', accelerator:'CmdOrCtrl+P', click:() => { ipcSend('toggle-preview'); }},
        {label:'Toggle Full Screen', accelerator:'F11', click:() => { ipcSend('mazimize'); }},
        {type:'separator'},
        {label:'Themes' },
        {type:'separator'},
        {label:'Preview Mode', submenu: [
            {label:'Markdown', click:() => { ipcSend('markdown-preview'); }},
            {label:'HTML', click:() => { ipcSend('html-preview'); }}
        ]},
        {type:'separator'},
        {role: 'toggledevtools'}
    ]},
  {label:'&Format', submenu: [
    {label:'Header', accelerator:'CmdOrCtrl+H', click:() => { ipcSend('insert-heading'); }},
    {label:'Bold', accelerator:'CmdOrCtrl+B', click:() => { ipcSend('insert-bold'); }},
    {label:'Italics', accelerator:'CmdOrCtrl+I', click:() => { ipcSend('insert-italic'); }},
    {label:'Strikethrough', accelerator:'CmdOrCtrl+-', click:() => { ipcSend('insert-strikethrough'); }},
    {type:'separator'},
    {label:'Insert Link', accelerator:'CmdOrCtrl+L', click:() => { ipcSend('insert-link'); }},
    {label:'Insert Image', accelerator:'CmdOrCtrl+K', click:() => { ipcSend('insert-image'); }},
    {label:'Insert Table', accelerator:'CmdOrCtrl+T', click:() => { ipcSend('insert-table') }},
    {label:'Insert Horizontal Rule', accelerator:'CmdOrCtrl+_', click:() => { ipcSend('insert-hr'); }},
    {label:'Insert Emoji...', accelerator:'CmdOrCtrl+E', click:() => { ipcSend('insert-emoji'); }},
    {type:'separator'},
    {label:'Toggle Code', accelerator:'CmdOrCtrl+;', click:() => { ipcSend('insert-code'); }},
    {label:'Toggle Comment', accelerator:'CmdOrCtrl+/', click:() => { ipcSend('insert-comment'); }},
    {label:'Toggle Blockquote', accelerator:'CmdOrCtrl+\'', click:() => { ipcSend('insert-quote'); }},
    {label:'Toggle Unordered List', click:() => { toggleUnorderedList(); }},
    {label:'Toggle Ordered List', click:() => { toggleOrderedList(); }},
    {type:'separator'},
    {label:'Insert YAML-frontmatter', accelerator:'CmdOrCtrl+Shift+Y', click:() => { ipcSend('insert-yaml'); }}
  ]},
    {role:'window', label:'&Window', submenu: [
        {label:'Minimize', click:() => {
            BrowserWindow.getFocusedWindow().minimize();
        }},
        {label:'Zoom', click:() => {
            toggleMaximize();
        }},
        {type:'separator'},
        {label:'Bring to Front', click:() => {
            windows[0].show();
        }}
    ]},
    {label:'&Help', role: 'help', submenu: [
        // {label:'Markdown Help', click:() => {
        //     ipcSend('markdown-modal');
        // }},
        // {type:'separator'},
        {label:'Documentation', click:() => {
            shell.openExternal(packageJSON.docs);
        }},
        {label:'Keybindings', click:() => {
            shell.openExternal(packageJSON.keybindings);
        }},
        {label:'Report Issue', click:() => {
            shell.openExternal(packageJSON.repository.bugs);
        }},
    {type:'separator'},
    {label:'Markdown Help', click:() => {
      ipcSend('markdown-modal');
    }},
        {type:'separator'},
        {label:'About Hyde', click:() => {
            ipcSend('about-modal');
        }}
    ]}
];
if (process.platform === 'darwin') {
  const name = app.getName();
  template.unshift({
    label:name,
    submenu: [
      {label:'About Hyde', click:() => {
        ipcSend('about-modal');
      }},
      {type:'separator'},
      {role: 'preferences', label:'Preferences', accelerator:'CmdOrCtrl+,', click:() => { ipcSend('toggle-settings'); }},
      {type:'separator'},
      {role: 'services', submenu: []},
      {type:'separator'},
      {role: 'hide'},
      {role: 'hideothers'},
      {role: 'unhide'},
      {type:'separator'},
      {role: 'quit'}
    ]
  })
  template[1].submenu[9] = {label:"Show in Finder", click:() => { ipcSend('open-file-manager'); }};
  template[1].submenu.splice(11,1);
  template[1].submenu[12] = {label:'Close', accelerator:'CmdOrCtrl+Q', click:() => { ipcSend('win-close'); }};
  template[3].submenu.splice(2,1);
  // Add syntax-themes to menu
  template[3].submenu[6].submenu = menuThemes();
  // Window menu
  template[5].submenu = [
    {role: 'minimize'},
    {role: 'zoom'},
    {type:'separator'},
    {role: 'front'}
  ]
} else {
  template[0].submenu[7] = {label:"Show in Explorer", click:() => { ipcSend('open-file-manager'); }};
  template[2].submenu[7].submenu = menuThemes();
}

function ipcSend(cmd) {
  var focusedWindow = BrowserWindow.getFocusedWindow();
  focusedWindow.webContents.send(cmd);
}

// Register local keyboard shortcuts
localShortcut.register('CmdOrCtrl+Shift+a', () => { ipcSend('auto-indent'); });
localShortcut.register('CmdOrCtrl+b', () => { ipcSend('insert-bold'); });
localShortcut.register('CmdOrCtrl+d', () => { ipcSend('select-word'); });
localShortcut.register('CmdOrCtrl+e', () => { ipcSend('insert-emoji'); });
localShortcut.register('CmdOrCtrl+f', () => { ipcSend('search-find'); });
localShortcut.register('CmdOrCtrl+Shift+f', () => { ipcSend('search-replace'); });
localShortcut.register('CmdOrCtrl+h', () => { ipcSend('insert-heading'); });
localShortcut.register('CmdOrCtrl+i', () => { ipcSend('insert-italic'); });
localShortcut.register('CmdOrCtrl+k', () => { ipcSend('insert-image'); });
localShortcut.register('CmdOrCtrl+l', () => { ipcSend('insert-link'); });
localShortcut.register('CmdOrCtrl+m', () => { ipcSend('toggle-menu'); });
localShortcut.register('CmdOrCtrl+n', () => { ipcSend('file-new'); });
localShortcut.register('CmdOrCtrl+o', () => { ipcSend('file-open'); });
localShortcut.register('CmdOrCtrl+Shift+o', () => { ipcSend('file-open-new'); });
localShortcut.register('CmdOrCtrl+p', () => { ipcSend('toggle-preview'); });
localShortcut.register('CmdOrCtrl+Shift+p', () => { ipcSend('toggle-palette'); });
localShortcut.register('CmdOrCtrl+r', () => { ipcSend('win-reload'); });
localShortcut.register('CmdOrCtrl+s', () => { ipcSend('file-save'); });
localShortcut.register('CmdOrCtrl+Shift+s', () => { ipcSend('file-save-as'); });
localShortcut.register('CmdOrCtrl+t', () => { ipcSend('table-modal'); });
localShortcut.register('CmdOrCtrl+up', () => { ipcSend('page-up'); });
localShortcut.register('CmdOrCtrl+down', () => { ipcSend('page-down'); });
localShortcut.register('CmdOrCtrl+left', () => { ipcSend('indent-less'); });
localShortcut.register('CmdOrCtrl+right', () => { ipcSend('indent-more'); });
localShortcut.register('CmdOrCtrl+-', () => { ipcSend('insert-strikethrough'); });
localShortcut.register('CmdOrCtrl+Shift+-', () => { ipcSend('insert-hr'); });
localShortcut.register('CmdOrCtrl+/', () => { ipcSend('insert-comment'); });
localShortcut.register('CmdOrCtrl+;', () => { ipcSend('insert-code'); });
localShortcut.register("CmdOrCtrl+'", () => { ipcSend("insert-quote"); });
localShortcut.register('CmdOrCtrl+.', () => { ipcSend('toggle-toolbar'); });
localShortcut.register('CmdOrCtrl+,', () => { ipcSend('toggle-settings'); });
localShortcut.register('CmdOrCtrl+Shift+/', () => { ipcSend('markdown-modal'); });

// Called after initialization
app.on('ready', function() {
  // Create native application menu for OSX
  Menu.setApplicationMenu(Menu.buildFromTemplate(template));
  // Initialize windowStateManager
  windowState = windowStateManager({
    defaultWidth: config.get('windowWidth'),
    defaultHeight: config.get('windowHeight')
  });
  settings.set('targetFile', readFile);
  // Create main window
  mainWindow = createWindow();
  tray.create(mainWindow);
  // Show DevTools
  if(args.dev) {
    require('devtron').install();
    mainWindow.webContents.openDevTools();
  }
  
  // FIXME: Window is still being removed from window.windows array
  mainWindow.on('close', (e) => {
    if(process.platform === 'darwin') {
      if(hasChanges) {
        var prompt = require('electron').dialog.showMessageBox({
            type:'question',
            buttons: ['Yes', 'No', 'Cancel'],
            title: 'Confirm',
            message: 'Unsaved Changes',
            detail: 'The current file contains unsaved changes, would you like to save them?'
         });
        if(prompt == 1) {  // NO
          e.preventDefault();
          hasChanges = false;
          mainWindow.close();
        } else if(prompt == 2) { // CANCEL
          e.preventDefault();
        } else {  // YES
          e.preventDefault();
          windows
          ipcSend('file-save');
        }
      }
    }
  });
});

ipc.on('changed-state', (state) => {
  // hasChanges = settings.get('hasChanges');
  hasChanges = state;
});

// Listen for uncaughtExceptions
process.on('uncaughtException', (err) => {
  console.error(err)
});

app.on('open-file', (e, path) => {
  e.preventDefault()
  if (isReady) {
    createWindow(path)
  } else {
    pathToOpen = path
  }
});

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On OSX it is common for applications and their menu bar to stay active until the user quits explicitly with Cmd+Q
  // if(process.platform !== 'darwin') {
    app.quit();
  // }
});

app.on('quit', (e) => {
  if(settings.get('keepInTray') && !isQuitting) {
    e.preventDefault();
    if(process.platform === 'darwin')
      app.hide();
    else
      Object.values(window.windows).hide();
  }
});

app.on('activate', () => {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow();
  }
});

app.on('before-quit', () => {
  windowState.saveState(mainWindow)
  isQuitting = true;
});

const appVersion = exports.appVersion = () => {
  return app.getVersion();
}

app.on('browser-window-created', function(event, win) {
  win.webContents.on('context-menu', function(e, params) {
    rightClickPos = { x: params.x, y: params.y };
    contextMenu.popup(win, params.x, params.y);
  });
});

ipc.on('show-context-menu', function(event) {
  const win = BrowserWindow.fromWebContents(event.sender);
  contextMenu.popup(win);
});

let rightClickPos = null;
const contextMenu = new Menu();
contextMenu.append(new MenuItem({ role: 'undo' }))
contextMenu.append(new MenuItem({ role: 'redo' }))
contextMenu.append(new MenuItem({ type:'separator' }))
contextMenu.append(new MenuItem({ label:"Cut", role: "cut" }))
contextMenu.append(new MenuItem({ label:"Copy", role: "copy" }))
contextMenu.append(new MenuItem({ label:"Paste", role: "paste" }))
contextMenu.append(new MenuItem({ label:"Select All", click:() => { ipcSend('ctrl+a'); } }))
contextMenu.append(new MenuItem({ type:'separator' }))
contextMenu.append(new MenuItem({ label:'Show in File Manager', click:() => { ipcSend('open-file-manager'); } }))
contextMenu.append(new MenuItem({ type:'separator' }))
contextMenu.append(new MenuItem({ label:'Inspect Element', click:() => { mainWindow.inspectElement(rightClickPos.x, rightClickPos.y); }}))