Implement persistent buffer

This commit is contained in:
Jonatan Heyman 2023-01-19 00:20:50 +01:00
parent c4e78a70ce
commit 14152fbede
10 changed files with 171 additions and 48 deletions

1
electron/constants.ts Normal file
View File

@ -0,0 +1 @@
export const WINDOW_CLOSE_EVENT = "window-close"

View File

@ -1,10 +1,21 @@
/*export default `text import { isMac } from "./detect-platform.js"
kukjavascript
ojprutt
hejpython
f = lambda: 2 +1`;*/
export default ` const modChar = isMac ? "⌘" : "Ctrl"
export const initialContent = `
text
Welcome to Heynote!
[${modChar} + Enter] Insert new note block at cursor
[${modChar} + L] Change block language
[${modChar} + Down] Goto next block
[${modChar} + Up] Goto previous block
[${modChar} + A] Select all text in a note block. Press again to select the whole scratchpad
[${modChar} + + Up/Down]  Add additional cursor above/below
text-a
`
export const initialDevContent = `
text-a text-a
Welcome to Heynote! Welcome to Heynote!
@ -98,4 +109,4 @@ Shopping list:
- Milk - Milk
- Eggs - Eggs
- Bread - Bread
- Cheese`; - Cheese`

View File

@ -1,7 +1,10 @@
import { app, BrowserWindow, shell, ipcMain, Menu, nativeTheme } from 'electron' import { app, BrowserWindow, shell, ipcMain, Menu, nativeTheme } from 'electron'
import { release } from 'node:os' import { release } from 'node:os'
import { join } from 'node:path' import { join } from 'node:path'
import * as jetpack from "fs-jetpack";
import menu from './menu' import menu from './menu'
import { initialContent, initialDevContent } from '../initial-content'
import { WINDOW_CLOSE_EVENT } from '../constants';
// The built directory structure // The built directory structure
// //
@ -43,6 +46,9 @@ let win: BrowserWindow | null = null
const preload = join(__dirname, '../preload/index.js') const preload = join(__dirname, '../preload/index.js')
const url = process.env.VITE_DEV_SERVER_URL const url = process.env.VITE_DEV_SERVER_URL
const indexHtml = join(process.env.DIST, 'index.html') const indexHtml = join(process.env.DIST, 'index.html')
const isDev = !!process.env.VITE_DEV_SERVER_URL
let contentSaved = false
async function createWindow() { async function createWindow() {
win = new BrowserWindow({ win = new BrowserWindow({
@ -61,6 +67,15 @@ async function createWindow() {
}, },
}) })
win.on("close", (event) => {
// Prevent the window from closing, and send a message to the renderer which will in turn
// send a message to the main process to save the current buffer and close the window.
if (!contentSaved) {
event.preventDefault()
}
win?.webContents.send(WINDOW_CLOSE_EVENT)
})
//nativeTheme.themeSource = "light" //nativeTheme.themeSource = "light"
if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298 if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298
@ -108,26 +123,35 @@ app.on('activate', () => {
} }
}) })
// New window example arg: new windows url
ipcMain.handle('open-win', (_, arg) => {
const childWindow = new BrowserWindow({
webPreferences: {
preload,
nodeIntegration: true,
contextIsolation: false,
},
})
if (process.env.VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${url}#${arg}`)
} else {
childWindow.loadFile(indexHtml, { hash: arg })
}
})
ipcMain.handle('dark-mode:set', (event, mode) => { ipcMain.handle('dark-mode:set', (event, mode) => {
nativeTheme.themeSource = mode nativeTheme.themeSource = mode
}) })
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource) ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
const bufferPath = isDev ? join(app.getPath("userData"), "buffer-dev.txt") : join(app.getPath("userData"), "buffer.txt")
ipcMain.handle('buffer-content:load', async () =>  {
if (jetpack.exists(bufferPath) === "file") {
return await jetpack.read(bufferPath, 'utf8')
} else {
return isDev? initialDevContent : initialContent
}
});
async function save(content) {
return await jetpack.write(bufferPath, content, {
atomic: true,
mode: '600',
})
}
ipcMain.handle('buffer-content:save', async (event, content) =>  {
return await save(content)
});
ipcMain.handle('buffer-content:saveAndQuit', async (event, content) => {
await save(content)
contentSaved = true
app.quit()
})

View File

@ -1,6 +1,8 @@
const { contextBridge } = require('electron') const { contextBridge } = require('electron')
import darkMode from "./theme-mode" import darkMode from "./theme-mode"
import { isMac, isWindows, isLinux } from "./detect-platform" import { isMac, isWindows, isLinux } from "../detect-platform"
import { ipcRenderer } from "electron"
import { WINDOW_CLOSE_EVENT } from "../constants"
contextBridge.exposeInMainWorld("platform", { contextBridge.exposeInMainWorld("platform", {
isMac, isMac,
@ -9,6 +11,31 @@ contextBridge.exposeInMainWorld("platform", {
}) })
contextBridge.exposeInMainWorld('darkMode', darkMode) contextBridge.exposeInMainWorld('darkMode', darkMode)
contextBridge.exposeInMainWorld("heynote", {
quit() {
console.log("quitting")
//ipcRenderer.invoke("app_quit")
},
onWindowClose(callback) {
ipcRenderer.on(WINDOW_CLOSE_EVENT, callback)
},
buffer: {
async load() {
return await ipcRenderer.invoke("buffer-content:load")
},
async save(content) {
return await ipcRenderer.invoke("buffer-content:save", content)
},
async saveAndQuit(content) {
return await ipcRenderer.invoke("buffer-content:saveAndQuit", content)
},
}
})
function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) { function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
return new Promise((resolve) => { return new Promise((resolve) => {

38
package-lock.json generated
View File

@ -29,8 +29,10 @@
"@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-node-resolve": "^15.0.1",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"debounce": "^1.2.1",
"electron": "^22.0.2", "electron": "^22.0.2",
"electron-builder": "^23.6.0", "electron-builder": "^23.6.0",
"fs-jetpack": "^5.1.0",
"sass": "^1.57.1", "sass": "^1.57.1",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"vite": "^4.0.3", "vite": "^4.0.3",
@ -2192,6 +2194,12 @@
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true "dev": true
}, },
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
"dev": true
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -2830,6 +2838,36 @@
"node": ">=6 <7 || >=8" "node": ">=6 <7 || >=8"
} }
}, },
"node_modules/fs-jetpack": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-5.1.0.tgz",
"integrity": "sha512-Xn4fDhLydXkuzepZVsr02jakLlmoARPy+YWIclo4kh0GyNGUHnTqeH/w/qIsVn50dFxtp8otPL2t/HcPJBbxUA==",
"dev": true,
"dependencies": {
"minimatch": "^5.1.0"
}
},
"node_modules/fs-jetpack/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/fs-jetpack/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/fs-minipass": { "node_modules/fs-minipass": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",

View File

@ -47,8 +47,10 @@
"@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-node-resolve": "^15.0.1",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"debounce": "^1.2.1",
"electron": "^22.0.2", "electron": "^22.0.2",
"electron-builder": "^23.6.0", "electron-builder": "^23.6.0",
"fs-jetpack": "^5.1.0",
"sass": "^1.57.1", "sass": "^1.57.1",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"vite": "^4.0.3", "vite": "^4.0.3",

View File

@ -1,22 +1,5 @@
<script> <script>
import { ref, shallowRef } from 'vue'
import { HeynoteEditor } from '../editor/editor.js' import { HeynoteEditor } from '../editor/editor.js'
import testContent from "../editor/fixture.js"
const modChar = window.platform.isMac ? "⌘" : "Ctrl"
const initialContent = `
text
Welcome to Heynote!
[${modChar} + Enter] Insert new note block at cursor
[${modChar} + L] Change block language
[${modChar} + Down] Goto next block
[${modChar} + Up] Goto previous block
[${modChar} + A] Select all text in a note block. Press again to select the whole scratchpad
[${modChar} + + Up/Down]  Add additional cursor above/below
text-a
`
export default { export default {
props: [ props: [
@ -37,10 +20,20 @@ Welcome to Heynote!
this.$emit("openLanguageSelector") this.$emit("openLanguageSelector")
}) })
// load buffer content and create editor
window.heynote.buffer.load().then((content) => {
this.editor = new HeynoteEditor({ this.editor = new HeynoteEditor({
element: this.$refs.editor, element: this.$refs.editor,
content: this.development ? testContent : initialContent, content: content,
theme: this.theme, theme: this.theme,
saveFunction: (content) => {
window.heynote.buffer.save(content)
},
})
})
// set up window close handler that will save the buffer and quit
window.heynote.onWindowClose(() => {
window.heynote.buffer.saveAndQuit(this.editor.getContent())
}) })
}, },

View File

@ -11,10 +11,11 @@ import { noteBlockExtension } from "./block/block.js"
import { changeCurrentBlockLanguage } from "./block/commands.js" import { changeCurrentBlockLanguage } from "./block/commands.js"
import { heynoteKeymap } from "./keymap.js" import { heynoteKeymap } from "./keymap.js"
import { languageDetection } from "./language-detection/autodetect.js" import { languageDetection } from "./language-detection/autodetect.js"
import { autoSaveContent } from "./save.js"
export class HeynoteEditor { export class HeynoteEditor {
constructor({element, content, focus=true, theme="light"}) { constructor({element, content, focus=true, theme="light", saveFunction=null}) {
this.element = element this.element = element
this.theme = new Compartment this.theme = new Compartment
@ -43,6 +44,8 @@ export class HeynoteEditor {
EditorView.editorAttributes.of((view) => { EditorView.editorAttributes.of((view) => {
return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"} return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"}
}), }),
saveFunction ? autoSaveContent(saveFunction, 2000) : [],
], ],
}) })
@ -77,6 +80,10 @@ export class HeynoteEditor {
setCurrentLanguage(lang, auto=false) { setCurrentLanguage(lang, auto=false) {
changeCurrentBlockLanguage(this.view.state, this.view.dispatch, lang, auto) changeCurrentBlockLanguage(this.view.state, this.view.dispatch, lang, auto)
} }
getContent() {
return this.view.state.sliceDoc()
}
} }

20
src/editor/save.js Normal file
View File

@ -0,0 +1,20 @@
import { ViewPlugin } from "@codemirror/view"
import { debounce } from "debounce"
export const autoSaveContent = (saveFunction, interval) => {
const save = debounce((view) => {
//console.log("saving buffer")
saveFunction(view.state.sliceDoc())
}, interval);
return ViewPlugin.fromClass(
class {
update(update) {
if (update.docChanged) {
save(update.view)
}
}
}
)
}