~dricottone/noticable

0a6377c125879214ce5190763e811a8d6f6f75f4 — Dominic Ricottone 2 years ago 21a3bc3
0.2 rewrite
4 files changed, 713 insertions(+), 329 deletions(-)

M index.html
M main.js
M preload.js
M renderer.js
M index.html => index.html +1 -9
@@ 10,19 10,11 @@
  </head>
  <body>
    <div class="sidebar">
      <div id="settings">
        <p>Editor Mode:</p>
        <input id="ui-focus-editor" type="checkbox" checked onchange="uiToggleEditor()">
        <label for="ui-focus-editor"></label>
        <p>Enable Rendering:</p>
        <input id="ui-enable-render" type="checkbox" checked>
        <label for="ui-enable-render"></label>
      </div>
      <ul id="list-filenames"></ul>
    </div>
    <div class="container focused" id="container-editor"></div>
    <!--NOTE: .markdown-body is the target for Github Markdown CSS module; do not modify!-->
    <div class="container markdown-body" id="container-rendered"></div>
    <div class="container markdown-body" id="container-viewer"></div>
    <script src="node_modules/jquery/dist/jquery.min.js"></script>
    <script src="node_modules/monaco-editor/min/vs/loader.js"></script>
    <script src="renderer.js"></script>

M main.js => main.js +274 -63
@@ 1,48 1,280 @@
// to disable logging, comment out the console.log line
function debug(message) {
  //console.log("[main] " + message);
}
////////////////////////////
// Global state goes here //
////////////////////////////

// constants
const { app, BrowserWindow, ipcMain, dialog, shell, Menu } = require("electron");
const os = require("os");
const path = require("path");
let win;
const os = require("os");
var win;

const dirNotes = path.join(os.homedir(), "notes");

const filePreload = path.join(__dirname, "preload.js");
const fileIndex = path.join(__dirname, "index.html");

const urlProject = "https://github.com/dricottone/noticable";
const urlBugTracker = "https://github.com/dricottone/noticable/issues";

// Options for prompt to discard or save changes.
const optionsDiscard = {
  message: "You have made changes in the editor. Do you want to discard those changes, or save to a file?",
  buttons: ["&Discard", "&Save", "Save &As..."],
  type: "question",
  normalizeAccessKeys: true,
};

// Options for error message that saving failed.
const optionsReSaveAs = {
  message: "File could not be saved.",
  buttons: ["&Cancel", "&Try Again"],
  type: "error",
  normalizeAccessKeys: true,
};

// Options for prompt to save a file.
const optionsSaveAs = {
  title: "Create new note",
  defaultPath: dirNotes,
  properties: ["showOverwriteConfirmation"],
  filters: [
    { name: "Markdown", extensions: ["md"] },
    { name: "Plain Text", extensions: ["txt"] },
  ],
};


// configuration
const notesDir = path.join(os.homedir(), "notes");
///////////////////////
// Functions go here //
///////////////////////

// Push filenames to be relative to the notes directory.
function relativeNotePath(filename) {
  return path.relative(dirNotes, filename);
}

// Prompt to save a file.
function promptSave() {
  dialog.showSaveDialog(win, optionsSaveAs)
  .then(r => {
    if (r.canceled) {
      announceFileNotSaved();
    } else {
      preloadSaveFile(relativeNotePath(r.filePath));
    }
  });
};

// Prompt to save a file after already attempting to do so.
function rePromptSave() {
  dialog.showMessageBox(win, optionsSaveError)
  .then(r => {
    if (r.response==0) {
      announceFileNotSaved();
    } else {
      dialog.showSaveDialog(win, optionsSaveAs)
      .then(r => {
        if (r.canceled) {
          announceFileNotSaved();
        } else {
          preloadReSaveFile(relativeNotePath(r.filePath));
        }
      });
    }
  });
};

// Prompt to save a file then open a new file. Discard the file if the prompt
// is cancelled.
function promptSaveDiscardableThenNew() {
  dialog.showMessageBox(win, optionsDiscard)
  .then(r => {
    if (r.response==0) {
      announceFileDiscardedForNewFile();
    } else if (r.response==1) {
      preloadTrySaveFileThenNewFile();
    } else {
      dialog.showSaveDialog(win, optionsSaveAs)
      .then(r => {
        if (r.canceled) {
          announceFileDiscardedForNewFile();
        } else {
          preloadSaveFileThenNewFile(relativeNotePath(r.filePath));
        }
      });
    }
  });
};

// Prompt to save a file then read a file. Discard the file if the prompt is
// cancelled.
function promptSaveDiscardableThenRead(filename) {
  dialog.showMessageBox(win, optionsDiscard)
  .then(r => {
    if (r.response==0) {
      announceFileDiscardedForReadFile(filename);
    } else if (r.response==1) {
      preloadTrySaveFileThenReadFile(filename);
    } else {
      dialog.showSaveDialog(win, optionsSaveAs)
      .then(r => {
        if (r.canceled) {
          announceFileDiscardedForReadFile(filename);
        } else {
          preloadSaveFileThenReadFile(relativeNotePath(r.filePath), filename);
        }
      });
    }
  });
};

// Prompt to save a file after already attempting to do so. Discard the file if
// the prompt is cancelled.
function rePromptSaveDiscardableThenNew() {
  dialog.showMessageBox(win, optionsSaveError)
  .then(r => {
    if (r.response==0) {
      announceFileDiscardedForNewFile();
    } else {
      dialog.showSaveDialog(win, optionsSaveAs)
      .then(r => {
        if (r.canceled) {
          announceFileDiscardedForNewFile();
        } else {
          preloadReSaveFileThenNewFile(relativeNotePath(r.filePath));
        }
      });
    }
  });
};

// Ask preload to save a file.
function preloadSaveFile(filename) {
  win.webContents.send("saveFile", filename);
};

// Ask preload to *try* to save a file.
// NOTE: We don't know if preload has a cached file name. There is no real
//       advantage to querying this first. Same number of IPC calls in worst
//       case (i.e. no known file name) and excessive calls in best case.
function preloadTrySaveFile() {
  win.webContents.send("trySaveFile", "");
};

// Ask preload to save a file.
// NOTE: Triggers different logic. Preload skips querying the renderer for the
//       note (because this is a *re*-save).
function preloadReSaveFile(filename) {
  win.webContents.send("reSaveFile", filename);
};

// Ask preload to save a file then show a new file.
function preloadSaveFileThenNewFile(filename) {
  win.webContents.send("saveFileThenNewFile", filename);
};

// Ask preload to *try* to save a file then show a new file.
// NOTE: We don't know if preload has a cached file name. There is no real
//       advantage to querying this first. Same number of IPC calls in worst
//       case (i.e. no known file name) and excessive calls in best case.
function preloadTrySaveFileThenNewFile() {
  win.webContents.send("trySaveFileThenNewFile", "");
};

// Ask preload to save a file then show a new file.
// NOTE: Triggers different logic. Preload skips querying the renderer for the
//       note (because this is a *re*-save).
function preloadReSaveFileThenNewFile(toSaveFilename, toReadFilename) {
  win.webContents.send("reSaveFileThenNewFile", { toSave: toSaveFilename, toRead: toReadFilename });
};

// Ask preload to save a file then read another file.
function preloadSaveFileThenReadFile(toSaveFilename, toReadFilename) {
  win.webContents.send("saveFileThenReadFile", { toSave: toSaveFilename, toRead: toReadFilename });
};

// Ask preload to *try* to save a file then read another file.
// NOTE: We don't know if preload has a cached file name. There is no real
//       advantage to querying this first. Same number of IPC calls in worst
//       case (i.e. no known file name) and excessive calls in best case.
function preloadTrySaveFileThenReadFile(filename) {
  win.webContents.send("trySaveFileThenReadFile", filename);
};

// Announce that a file was not saved.
function announceFileNotSaved() {
  win.webContents.send("fileNotSaved", "");
};

// Announce that a file was not saved and changes should be discarded for a new
// file.
function announceFileDiscardedForNewFile() {
  win.webContents.send("fileDiscardedForNewFile", "");
};

// Announce that a file was not saved and changes should be discarded for
// another file to be read.
function announceFileDiscardedForReadFile(filename) {
  win.webContents.send("fileDiscardedForReadFile", filename);
};

// Ask renderer to sent editor content to be checked against the cached content
// then reset the editor.
function preloadRendererSendContentForCheckThenNew() {
  win.webContents.send("rendererSendContentForCheckThenNew", "");
};

// Ask renderer to send content for rendering.
function preloadRendererSendContentForRender() {
  win.webContents.send("rendererSendContentForRender", "");
};

// Ask renderer to show the editor.
function preloadRendererShowEditor() {
  win.webContents.send("rendererShowEditor", "");
};

// Ask renderer to send content for rendering and show the viewer.
function preloadRendererShowViewer() {
  preloadRendererSendContentForRender();
  win.webContents.send("rendererShowViewer", "");
};


//////////////////////////////
// Electron magic goes here //
//////////////////////////////

// manu
const template = [
  {
    label: "File",
    submenu: [
      {
        label: "Save and Render",
        label: "Save",
        accelerator: "CmdOrCtrl+S",
        click: () => {
          win.webContents.send("menu-render-markdown", "");
        }
        click: preloadTrySaveFile,
      },
      {
        label: "Save",
        label: "Save As...",
        accelerator: "CmdOrCtrl+Shift+S",
        click: () => {
          win.webContents.send("menu-save-text", "");
          promptSave(win);
        }
      },
      {
        label: "Render Note",
        accelerator: "CmdOrCtrl+R",
        click: preloadRendererSendContentForRender,
      },
      {
        label: "New",
        accelerator: "CmdOrCtrl+N",
        click: () => {
          win.webContents.send("menu-new-file", "");
        }
        click: preloadRendererSendContentForCheckThenNew,
      },
      { type: "separator" },
      {
        label: "Show Notes Directory",
        click: async () => {
          await shell.openPath(notesDir);
          await shell.openPath(dirNotes);
        }
      },
      { type: "separator" },


@@ 67,24 299,21 @@ const template = [
    label: "View",
    submenu: [
      {
        label: "Focus Editor Mode",
        label: "Show Editor",
        accelerator: "CmdOrCtrl+E",
        click: () => {
          win.webContents.send("menu-focus-editor", "");
        }
        click: preloadRendererShowEditor,
      },
      {
        label: "Focus Rendered Mode",
        label: "Show Viewer",
        accelerator: "CmdOrCtrl+Shift+E",
        click: () => {
          win.webContents.send("menu-unfocus-editor", "");
        }
        click: preloadRendererShowViewer,
      },
      { type: "separator" },
      { role: "reload" },
      { role: "forceReload" },
      { role: "toggleDevTools" },
      { type: "separator" },
      // NOTE: I believe these should not be enabled in a production build
      // { role: "reload" },
      // { role: "forceReload" },
      // { role: "toggleDevTools" },
      // { type: "separator" },
      { role: "resetZoom" },
      { role: "zoomIn" },
      { role: "zoomOut" },


@@ 104,13 333,13 @@ const template = [
      {
        label: "About",
        click: async () => {
          await shell.openExternal("https://github.com/dricottone/noticable");
          await shell.openExternal(urlProject);
        }
      },
      {
        label: "Report Bugs",
        click: async () => {
          await shell.openExternal("https://github.com/dricottone/noticable/issues");
          await shell.openExternal(urlBugTracker);
        }
      }
    ]


@@ 118,27 347,6 @@ const template = [
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));

// messaging
function postFileName(window, event) {
  dialog.showSaveDialog(window, {
    title: "Create new note",
    defaultPath: notesDir,
    properties: ["showOverwriteConfirmation"],
    filters: [
      { name: "Markdown", extensions: ["md"] },
      { name: "Plain Text", extensions: ["txt"] },
    ]
  })
  .then(r => {
    if (!r.canceled) {
      filename = r.filePath
      debug("posting new filename '" + filename + "'...");
      event.sender.send("post-new-filename", filename);
    }
  });
}

// utilities
function initializeWindow() {
  win = new BrowserWindow({
    width: 800,


@@ 146,23 354,26 @@ function initializeWindow() {
    webPreferences: {
      contextIsolation: true,
      nodeIntegration: false,
      preload: path.join(__dirname, "preload.js")
      preload: filePreload,
    }
  });
  win.loadFile(fileIndex);

  win.loadFile(path.join(__dirname, "index.html"));

  ipcMain.on("request-local-filename", (event) => {
    debug("caught request for local filename");
    postFileName(win, event);
  });
  ////////////////////////////
  // Listen for events here //
  ////////////////////////////
  ipcMain.on("promptSave", promptSave);
  ipcMain.on("promptSaveDiscardableThenNew", promptSaveDiscardableThenNew);
  ipcMain.on("promptSaveDiscardableThenRead", (_, filename) => promptSaveDiscardableThenRead(filename));
  ipcMain.on("rePromptSave", rePromptSave);
  //ipcMain.on("fileUnreadable", () => {});
  //ipcMain.on("fileUnunwritable", () => {});

  win.on("closed", () => {
    win = null;
  });
};

// app event loop
app.on("ready", initializeWindow);

app.on("window-all-closed", () => {

M preload.js => preload.js +310 -154
@@ 1,186 1,342 @@
// to disable logging, comment out the console.log line
function debug(message) {
  //console.log("[preload] " + message);
}
////////////////////////////
// Global state goes here //
////////////////////////////

// constants
const { ipcRenderer } = require("electron");
const fs = require("fs");
const os = require("os");
const path = require("path");
const md = require("markdown-it")({ html: true });
let currentFilename = "";
let currentFileContent = "";

// configuration
var currentFile = "";
var currentNote = "";
var currentNotes = [];
const newFileButton = "+ New Note";
const notesDir = path.join(os.homedir(), "notes");

// messaging
function requestLocalFilename() {
  debug("requesting local filename...");
  ipcRenderer.send("request-local-filename", "");
}
function postFileText(text) {
  debug("posting file text...");
  window.postMessage({ type: "post-file-text", text: text }, "*");
}
function postDisplayFilename(filename) {
  debug("posting display file name '" + filename + "'...");
  window.postMessage({ type: "post-display-filename", text: filename }, "*");
}
function postHTML(html) {
  debug("posting HTML...");
  window.postMessage({ type: "post-html", text: html }, "*");
}
function requestEditorText() {
  debug("requesting editor text...");
  window.postMessage({ type: "request-editor-text" }, "*");
}
function requestFocusEditor() {
  debug("requesting focus editor...");
  window.postMessage({ type: "request-focus-editor" }, "*");
}
function requestUnfocusEditor() {
  debug("requesting unfocus editor...");
  window.postMessage({ type: "request-unfocus-editor" }, "*");
}
function requestToggleEditor() {
  debug("requesting toggle editor focus...");
  window.postMessage({ type: "request-toggle-editor" }, "*");
}
function requestHighlightFilename(filename) {
  debug("requesting highlight filename...");
  window.postMessage({ type: "request-highlight-filename", text: filename }, "*");
}
function requestAlertInvalidFilename() {
  debug("requesting alert about invalid filename...");
  window.postMessage({ type: "request-alert-invalid-filename" }, "*");
}
const dirNotes = path.join(os.homedir(), "notes");

// utilities
function initNotesList() {
  // initialize the new file button
  postDisplayFilename(newFileButton);
  // initialize the notes files
  fs.readdir(notesDir, (err, files) => {
    if (err) throw err;
    files.forEach(file => {
      postDisplayFilename(getPrettyFilename(file));
    });
  });
}
function getActualFilename(filename) {
  unprettyFilename = path.basename(filename, ".md").split(" ").join("_") + ".md";
  return path.join(notesDir, unprettyFilename);
}
function getPrettyFilename(filename) {
  return path.basename(filename, ".md").split("_").join(" ");
}
function newFile() {
  updateState("","");
  requestLocalFilename();
}
function readFile(filename) {
  if (filename == newFileButton) {
    newFile()

///////////////////////
// Functions go here //
///////////////////////

// Push filenames to be relative to the notes directory.
// NOTE: In this module, we need to concatenate the notes directory and the
//       relative path. Different modules have different needs.
function relativeNotePath(filename) {
  return path.join(dirNotes, filename);
};

// Cache the new file name and ask renderer to send editor content to be saved.
function saveFile(filename) {
  currentFile = filename;
  rendererSendContentForSave();
};

// If the file name is cached, ask renderer to send editor content to be saved.
// Otherwise ask main to prompt for a new file name and proceed through
// `saveFile` logic.
function trySaveFile() {
  if (currentFile == "") {
    mainPromptSave();
  } else {
    let actualFilename = getActualFilename(filename);
    fs.readFile(actualFilename, "utf8", (err, content) => {
      if (err) requestAlertInvalidFilename();
      updateState(actualFilename,content);
      requestHighlightFilename(filename);
    });
    rendererSendContentForSave();
  }
}
function renderMarkdown(content) {
  postHTML(md.render(content));
}
function updateState(filename, content) {
  currentFilename = filename;
  currentFileContent = content;
  postFileText(content);
}
function saveFile(content) {
  if (currentFilename != "") {
    currentFileContent = content;
    debug("writing file '" + currentFilename + "'");
    fs.writeFile(currentFilename, currentFileContent, (err) => {
      if (err) throw err;
    });
};

// Cache the new file name and ask renderer to send editor content to be saved
// then reset the editor.
function saveFileThenNewFile(filename) {
  currentFile = filename;
  rendererSendContentForSaveThenNew();
};

// If the file name is cached, ask renderer to send editor content to be saved.
// Otherwise ask main to prompt for a new file name and proceed through
// `saveFileThenNewFile` logic.
function trySaveFileThenNewFile() {
  if (currentFile == "") {
    mainPromptSaveDiscardableThenNew();
  } else {
    debug("no filename; halting write file");
    rendererSendContentForSaveThenNew();
  }
}
function saveFileConditional(content) {
  if (currentFilename != "") {
    if (content != currentFileContent || currentFileContent == "") {
      currentFileContent = content;
      debug("writing updated file '" + currentFilename + "'");
      fs.writeFile(currentFilename, currentFileContent, (err) => {
        if (err) throw err;
      });
      return true;
};

// If the file name is cached, save the cached content to it. Otherwise ask
// main to prompt for a new file name and proceed through
// `saveFileThenReadFile` logic.
function trySaveFileThenReadFile(filename) {
  if (currentFile == "") {
    mainPromptSaveDiscardableThenRead(filename);
  } else {
    writeFileThenReadFile(currentFile, currentNote, filename);
  }
};

// Read a directory and return a sorted array of all file names.
function readNotesDirectory(directory) {
  let files = fs.readdirSync(directory);
  return files.sort((a,b) => a.localeCompare(b));
};

// Read a file and send the content and title to the renderer.
function readNoteFromFile(filename) {
  fs.readFile(relativeNotePath(filename), "utf8", (err, content) => {
    if (err) {
      announceFileUnreadable();
    } else {
      debug("no updates to file; halting write file");
      return false;
      currentFile = filename;
      currentNote = content;
      rendererNewTitle(filename);
      rendererNewContent(content);
      rendererNewHTML(md.render(content));
    }
  } else {
    debug("no filename; halting write file");
    return false;
  });
};

// Reset local cache and update renderer's state.
// NOTE: This is a destructive operation. Changes should have been saved or
//       discarded with user permission *first*.
function newNote() {
  currentFile = "";
  currentNote = "";
  rendererNewTitle("");
  rendererNewContent("");
  rendererNewHTML("");
};

// Write a note to a file.
function writeFile(filename, content) {
  console.log("trying to save " + filename);
  fs.writeFile(relativeNotePath(filename), content, (err) => {
    if (err) {
      announceFileUnwritable();
      mainRePromptSave();
    } else {
      currentFile = filename;
      currentNote = content;
      rendererNewTitle(filename);
    }
  });
};

// Write a note to a file and then open a new file.
function writeFileThenNewFile(filename, content) {
  console.log("trying to save " + filename);
  fs.writeFile(relativeNotePath(filename), content, (err) => {
    if (err) {
      announceFileUnwritable();
      mainRePromptSaveThenNew();
    } else {
      rendererAddTitle(filename);
      newNote()
    }
  });
};

// Write a note to a file and then read another file.
function writeFileThenReadFile(toWriteFilename, content, toReadFilename) {
  console.log("trying to save " + toWriteFilename);
  fs.writeFile(relativeNotePath(toWriteFilename), content, (err) => {
    if (err) {
      announceFileUnwritable();
      mainRePromptSaveThenRead();
    } else {
      rendererAddTitle(toWriteFilename);
      readNoteFromFile(toReadFilename);
    }
  });
};

// Ask renderer to show the editor.
function rendererShowEditor() {
  window.postMessage({ type: "showEditor" }, "*")
};

// Ask renderer to show the viewer.
function rendererShowViewer() {
  window.postMessage({ type: "showViewer" }, "*")
};

// Ask renderer to send editor content so that it can be saved.
function rendererSendContentForSave() {
  window.postMessage({ type: "sendContentForSave" }, "*")
};

// Ask renderer to send editor content so that it can be saved and then
// reset.
function rendererSendContentForSaveThenNew() {
  window.postMessage({ type: "sendContentForSaveThenNew" }, "*")
};

// Ask renderer to send editor content so that it can be checked and then
// conditionally reset.
function rendererSendContentForCheckThenNew() {
  window.postMessage({ type: "sendContentForCheckThenNew" }, "*")
};

// Ask renderer to send editor content so that it can be rendered.
function rendererSendContentForRender() {
  window.postMessage({ type: "sendContentForRender" }, "*")
};

// Ask renderer to add a note title to the sidebar.
function rendererAddTitle(filename) {
    if (currentNotes.indexOf(filename)==-1) {
      currentNotes.push(filename);
      window.postMessage({ type: "addTitle", text: filename }, "*")
    }
};

// Ask renderer to add a note title to the sidebar. Does not need to trigger a
// re-sort. Should only be used on initialization.
function rendererAddTitleOrdered(filename) {
  window.postMessage({ type: "addTitleOrdered", text: filename }, "*")
};

// Ask renderer to highlight a new note title in the sidebar.
function rendererNewTitle(filename) {
  if (filename!="") {
    rendererAddTitle(filename);
    window.postMessage({ type: "newTitle", text: filename }, "*")
  }
};

// Ask renderer to show new content in the editor.
function rendererNewContent(content) {
  window.postMessage({ type: "newContent", text: content }, "*")
};

// Ask renderer to show new HTML in the viewer.
function rendererNewHTML(html) {
  window.postMessage({ type: "newHTML", text: html }, "*")
};

// Ask main to prompt for a new file name.
function mainPromptSave() {
  ipcRenderer.send("promptSave", "");
};

// Ask main to prompt for either permission to discard changes or a new file
// name then open a new file.
function mainPromptSaveDiscardableThenNew() {
  ipcRenderer.send("promptSaveDiscardableThenNew", "");
};

// Ask main to prompt for either permission to discard changes or a new file
// name then read a new file.
function mainPromptSaveDiscardableThenRead(filename) {
  ipcRenderer.send("promptSaveDiscardableThenRead", filename);
};

// Ask main to prompt for a new file after the previous file name failed to
// work.
function mainRePromptSave() {
  ipcRenderer.send("rePromptSave", "");
};

// Ask main to prompt for a new file after the previous file name failed to
// write then open a new file.
function mainRePromptSaveThenNew() {
  ipcRenderer.send("rePromptSaveThenNew", "");
}

// listen to main process
ipcRenderer.on("post-new-filename", (event, filename) => {
  debug("caught file name '" + filename + "'");
  currentFilename = getActualFilename(filename);
  prettyFilename = getPrettyFilename(filename);
  postDisplayFilename(prettyFilename);
  requestHighlightFilename(prettyFilename);
});
ipcRenderer.on("menu-save-text", () => {
  debug("caught menu button for save text");
  requestEditorText();
});
ipcRenderer.on("menu-render-markdown", () => {
  debug("caught menu button for render markdown");
  requestEditorText();
  requestUnfocusEditor();
});
ipcRenderer.on("menu-new-file", () => {
  debug("caught menu button for new file");
  requestEditorText();
  newFile();
});
ipcRenderer.on("menu-focus-editor", () => {
  debug("caught menu button for focus editor");
  requestFocusEditor();
// Ask main to prompt for a new file after the previous file name failed to
// write then read another file.
function mainRePromptSaveThenRead(filename) {
  ipcRenderer.send("rePromptSaveThenRead", filename);
}

// Announce that a file is unreadable.
function announceFileUnreadable() {
  ipcRenderer.send("fileUnreadable", "");
  window.postMessage({ type: "fileUnreadable" }, "*")
};

// Announce that a file is unwritable.
function announceFileUnwritable() {
  ipcRenderer.send("fileUnwritable", "");
  window.postMessage({ type: "fileUnwritable" }, "*")
};

// Main announced that a file was not saved. Broadcast this announcement.
function broadcastFileNotSaved() {
  window.postMessage({ type: "fileNotSaved" }, "*")
};

// Main announced that a file was discarded. Broadcast this announcement.
function broadcastFileDiscarded() {
  window.postMessage({ type: "fileDiscarded" }, "*")
};


////////////////////////////
// Listen for events here //
////////////////////////////
ipcRenderer.on("saveFile", (_, filename) => saveFile(filename));
ipcRenderer.on("saveFileThenNewFile", (_, filename) => saveFileThenNewFile(filename));
ipcRenderer.on("saveFileThenReadFile", (_, filenames) => writeFileThenReadFile(filenames.toSave, currentNote, filenames.toRead));
ipcRenderer.on("trySaveFile", () => trySaveFile());
ipcRenderer.on("trySaveFileThenNewFile", () => trySaveFileThenNewFile());
ipcRenderer.on("trySaveFileThenReadFile", (_, filename) => trySaveFileThenReadFile(filename));
ipcRenderer.on("reSaveFile", (_, filename) => writeFile(filename, currentNote));
ipcRenderer.on("reSaveFileThenNewFile", (_, filename) => writeFileThenNewFile(filename, currentNote));
ipcRenderer.on("reSaveFileThenReadFile", (_, filenames) => writeFileThenReadFile(filenames.toSave, currentNote, filenames.toRead));
ipcRenderer.on("fileNotSaved", () => broadcastFileNotSaved());
ipcRenderer.on("fileDiscardedForNewFile", () => {
  broadcastFileDiscarded();
  newNote();
});
ipcRenderer.on("menu-unfocus-editor", () => {
  debug("caught menu button for unfocus editor");
  requestUnfocusEditor();
ipcRenderer.on("fileDiscardedForReadFile", (_, filename) => {
  broadcastFileDiscarded();
  readNoteFromFile(filename);
});
ipcRenderer.on("rendererShowViewer", () => rendererShowViewer());
ipcRenderer.on("rendererShowEditor", () => rendererShowEditor());
ipcRenderer.on("rendererSendContentForCheckThenNew", () => rendererSendContentForCheckThenNew());
ipcRenderer.on("rendererSendContentForRender", () => rendererSendContentForRender());

// listen to renderer
window.addEventListener("message", (event) => {
  if (event.source != window) return;
  if (event.data.type) {
    let parameter = event.data.text;
    switch(event.data.type) {
      case "post-editor-text":
        debug("caught editor text");
        saveFileConditional(event.data.text);
        renderMarkdown(event.data.text);
      case "contentForSave":
        writeFile(currentFile, parameter);
        break;
      case "contentForSaveThenNew":
        writeFileThenNewFile(currentFile, parameter);
        break;
      case "request-file-text":
        filename = event.data.text
        debug("caught request for text of file '" + filename + "'");
        readFile(filename);
      case "contentForSaveThenRead":
        writeFileThenReadFile(currentFile, parameter.note, parameter.title);
        break;
      case "contentForCheckThenNew":
        if (currentNote!=parameter) {
          currentNote = parameter.note;
          mainPromptSaveDiscardableThenNew();
        } else {
          newNote();
        }
        break;
      case "contentForCheckThenRead":
        if (currentNote!=parameter.note) {
          currentNote = parameter.note;
          mainPromptSaveDiscardableThenRead(parameter.title);
        } else {
          readNoteFromFile(parameter.title)
        }
        break;
      case "contentForRender":
        rendererNewHTML(md.render(parameter));
        rendererShowViewer();
        break;
    }
  }
}, false);

// initialize renderer
window.addEventListener("load", initNotesList);
window.addEventListener("load", () => {
  currentNotes = readNotesDirectory(dirNotes);
  currentNotes.forEach(filename => {
    rendererAddTitleOrdered(filename);
  });
});


M renderer.js => renderer.js +128 -103
@@ 1,9 1,6 @@
// to disable logging, comment out the console.log line
function debug(message) {
  //console.log("[renderer] " + message);
}

// initialize monaco editor
////////////////////////////
// Monaco magic goes here //
////////////////////////////
require.config({ paths: { vs: 'node_modules/monaco-editor/min/vs' } });
require(['vs/editor/editor.main'], function () {
  window.editor = monaco.editor.create(document.getElementById('container-editor'), {


@@ 17,109 14,137 @@ require(['vs/editor/editor.main'], function () {
  });
});

// messaging
function postEditorText() {
  debug("posting editor text...");
  window.postMessage({ type: "post-editor-text", text: window.editor.getValue() }, "*");
}
function requestFileText(filename) {
  debug("posting editor text and requesting text of file '" + filename + "'...");
  window.postMessage({ type: "post-editor-text", text: window.editor.getValue() }, "*");
  window.postMessage({ type: "request-file-text", text: filename }, "*");
}

// utilities
function buildFilename(filename) {
  var el = document.createElement("li");
  el.innerHTML = filename;
  el.addEventListener("click", () => { requestFileText(filename); });
  return el
}
function sortFilenames(a, b) {
  return ($(b).text()) < ($(a).text());
}
function focusEditor() {

///////////////////////
// Functions go here //
///////////////////////

// TODO: Add documentation for all functions.

// Parse a file name and return a pretty note title.
function cleanTitle(filename) {
  return filename.split(".")[0].split("_").join(" ");
};

function showEditor() {
  $("#container-editor").addClass("focused");
  $("#container-rendered").removeClass("focused");
  $("#ui-focus-editor").prop("checked", true);
}
function unfocusEditor() {
  if ($("#ui-enable-render").prop("checked")) {
    $("#container-editor").removeClass("focused");
    $("#container-rendered").addClass("focused");
    $("#ui-focus-editor").prop("checked", false);
  } else {
    debug("rendering disabled; halting unfocus editor");
    focusEditor();
  }
}
function toggleEditor() {
  if ($("#ui-focus-editor").prop("checked")) {
    unfocusEditor();
    postEditorText();
  } else {
    focusEditor();
  }
}
function uiToggleEditor() {
  //NOTE: Remember that the status accessed here is the 'new' status, i.e. after it had been toggled
  if ($("#ui-focus-editor").prop("checked")) {
    debug("UI focus editor");
    focusEditor();
  } else {
    debug("UI unfocus editor");
    unfocusEditor();
    postEditorText();
  }
}
  $("#container-viewer").removeClass("focused");
};

function showViewer() {
  $("#container-editor").removeClass("focused");
  $("#container-viewer").addClass("focused");
};

function addTitle(filename) {
  let element = document.createElement("li");
  element.innerHTML = cleanTitle(filename);
  element.addEventListener("click", () => { preloadContentForCheckThenRead(filename); });
  $("#list-filenames").append(element)
};

function newTitle(filename) {
  $("#list-filenames li").removeClass("focused")
  let selector = "#list-filenames li:contains(" + cleanTitle(filename) + ")"
  $(selector).addClass("focused")
};

function sortTitles() {
  let sidebar = $("#list-filenames")
  let titles = sidebar.children("li")
  titles.detach().sort((a,b) => a.innerText.localeCompare(b.innerText))
  sidebar.append(titles)
};

function newContent(content) {
  window.editor.setValue(content);
};

function newHTML(html) {
  $("#container-viewer").html(html);
};

function preloadContentForSave() {
  window.postMessage({ type: "contentForSave", text: window.editor.getValue() }, "*");
};

function preloadContentForSaveThenNew() {
  window.postMessage({ type: "contentForSaveThenNew", text: window.editor.getValue() }, "*");
};

function preloadContentForSaveThenRead(filename) {
  window.postMessage({ type: "contentForSaveThenRead", text: { note: window.editor.getValue(), title: filename } }, "*");
};

function preloadContentForCheckThenNew() {
  window.postMessage({ type: "contentForCheckThenNew", text: window.editor.getValue() }, "*");
};

// listen to preload
function preloadContentForCheckThenRead(filename) {
  window.postMessage({ type: "contentForCheckThenRead", text: { note: window.editor.getValue(), title: filename } }, "*");
};

function preloadContentForRender() {
  window.postMessage({ type: "contentForRender", text: window.editor.getValue() }, "*");
};


////////////////////////////
// Listen for events here //
////////////////////////////
window.addEventListener("message", (event) => {
  if (event.source != window) return;
  if (event.data.type) {
    let parameter = event.data.text;
    switch(event.data.type) {
      case "post-file-text":
        debug("caught file text");
        window.editor.setValue(event.data.text);
        focusEditor();
        break;
      case "post-display-filename":
        filename = event.data.text
        debug("caught display filename '" + filename + "'");
        $("#list-filenames").append(buildFilename(filename));
        $("#list-filenames li").sort(sortFilenames).appendTo("#list-filenames");
        break;
      case "post-html":
        debug("caught HTML");
        $("#container-rendered").html(event.data.text);
        break;
      case "request-editor-text":
        debug("caught request for editor text");
        postEditorText();
        break;
      case "request-focus-editor":
        debug("caught request to focus editor");
        focusEditor();
        break;
      case "request-unfocus-editor":
        debug("caught request to unfocus editor");
        unfocusEditor();
        break;
      case "request-toggle-editor":
        debug("caught request to toggle editor");
        toggleEditor();
        break;
      case "request-highlight-filename":
        filename = event.data.text;
        debug("caught request to highlight filename '" + filename + "'");
        $("#list-filenames li").removeClass("highlight");
        $("#list-filenames li").each( function(i, li) {
          if ( $(li).text()==filename) $(li).addClass("highlight");
        });
        break;
      case "request-alert-invalid-filename":
        debug("caught request to alert about invalid filename");
        alert("Error: File could not be read");
      case "addTitle":
        addTitle(parameter);
        sortTitles();
        break;
      case "addTitleOrdered":
        addTitle(parameter);
        break;
      case "newTitle":
        newTitle(parameter);
        break;
      case "newContent":
        newContent(parameter);
        break;
      case "newHTML":
        newHTML(parameter);
        break;
      case "sendContentForSave":
        preloadContentForSave();
        break;
      case "sendContentForSaveThenNew":
        preloadContentForSaveThenNew();
        break;
      case "sendContentForSaveThenRead":
        preloadContentForSaveThenRead(parameter);
        break;
      case "sendContentForCheckThenNew":
        preloadContentForCheckThenNew();
        break;
      case "sendContentForRender":
        preloadContentForRender();
        break;
      case "showEditor":
        showEditor();
        break;
      case "showViewer":
        showViewer();
        break;
      case "fileNotSaved":
        // TODO: add some UI component to indicate an error occured
        break;
      case "fileDiscarded":
        // TODO: add some UI component to indicate an error occured
        break;
      case "fileUnreadable":
        // TODO: add some UI component to indicate an error occured
        break;
      case "fileUnwritable":
        // TODO: add some UI component to indicate an error occured
        break;
    }
  }