diff --git a/assets/icons/arrow-right-black.svg b/assets/icons/arrow-right-black.svg
new file mode 100644
index 0000000..27ad162
--- /dev/null
+++ b/assets/icons/arrow-right-black.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right-grey.svg
similarity index 100%
rename from assets/icons/arrow-right.svg
rename to assets/icons/arrow-right-grey.svg
diff --git a/assets/icons/arrow-right-white.svg b/assets/icons/arrow-right-white.svg
new file mode 100644
index 0000000..9130299
--- /dev/null
+++ b/assets/icons/arrow-right-white.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index bef7a83..52f7c2f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -36,6 +36,8 @@ Available for Mac, Windows, and Linux.
⌥ + Shift + Enter Add new block at the start of the buffer
⌘ + ⌥ + Enter Split the current block at cursor position
⌘ + L Change block language
+⌘ + S Create a new note from the current block
+⌘ + P Open note selector
⌘ + Down Goto next block
⌘ + Up Goto previous block
⌘ + A Select all text in a note block. Press again to select the whole buffer
@@ -52,6 +54,8 @@ Ctrl + Shift + Enter Add new block at the end of the buffer
Alt + Shift + Enter Add new block at the start of the buffer
Ctrl + Alt + Enter Split the current block at cursor position
Ctrl + L Change block language
+Ctrl + S Create a new note from the current block
+Ctrl + P Open note selector
Ctrl + Down Goto next block
Ctrl + Up Goto previous block
Ctrl + A Select all text in a note block. Press again to select the whole buffer
diff --git a/electron/initial-content.ts b/electron/initial-content.ts
index d120afe..374f812 100644
--- a/electron/initial-content.ts
+++ b/electron/initial-content.ts
@@ -1,9 +1,8 @@
import os from "os";
import { keyHelpStr } from "../shared-utils/key-helper";
-export const eraseInitialContent = !!process.env.ERASE_INITIAL_CONTENT
-
export const initialContent = `
+{"formatVersion":"1.0.0","name":"Scratch"}
∞∞∞markdown
Welcome to Heynote! 👋
diff --git a/electron/main/buffer.js b/electron/main/buffer.js
deleted file mode 100644
index 0ecc5a7..0000000
--- a/electron/main/buffer.js
+++ /dev/null
@@ -1,172 +0,0 @@
-import fs from "fs"
-import os from "node:os"
-import { join, dirname, basename } from "path"
-import { app, ipcMain, dialog } from "electron"
-import * as jetpack from "fs-jetpack";
-
-import CONFIG from "../config"
-import { isDev } from "../detect-platform"
-import { win } from "./index"
-import { eraseInitialContent, initialContent, initialDevContent } from '../initial-content'
-
-const untildify = (pathWithTilde) => {
- const homeDirectory = os.homedir();
- return homeDirectory
- ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory)
- : pathWithTilde;
-}
-
-export function constructBufferFilePath(directoryPath, path) {
- return join(untildify(directoryPath), path)
-}
-
-export function getFullBufferFilePath(path) {
- let defaultPath = app.getPath("userData")
- let configPath = CONFIG.get("settings.bufferPath")
- let bufferPath = configPath.length ? configPath : defaultPath
- let bufferFilePath = constructBufferFilePath(bufferPath, path)
- try {
- // use realpathSync to resolve a potential symlink
- return fs.realpathSync(bufferFilePath)
- } catch (err) {
- // realpathSync will fail if the file does not exist, but that doesn't matter since the file will be created
- if (err.code !== "ENOENT") {
- throw err
- }
- return bufferFilePath
- }
-}
-
-
-export class Buffer {
- constructor({filePath, onChange}) {
- this.filePath = filePath
- this.onChange = onChange
- this.watcher = null
- this.setupWatcher()
- this._lastSavedContent = null
- }
-
- async load() {
- const content = await jetpack.read(this.filePath, 'utf8')
- this.setupWatcher()
- return content
- }
-
- async save(content) {
- this._lastSavedContent = content
- const saveResult = await jetpack.write(this.filePath, content, {
- atomic: true,
- mode: '600',
- })
- return saveResult
- }
-
- exists() {
- return jetpack.exists(this.filePath) === "file"
- }
-
- setupWatcher() {
- if (!this.watcher && this.exists()) {
- this.watcher = fs.watch(
- dirname(this.filePath),
- {
- persistent: true,
- recursive: false,
- encoding: "utf8",
- },
- async (eventType, filename) => {
- if (filename !== basename(this.filePath)) {
- return
- }
-
- // read the file content and compare it to the last saved content
- // (if the content is the same, then we can ignore the event)
- const content = await jetpack.read(this.filePath, 'utf8')
-
- if (this._lastSavedContent !== content) {
- // file has changed on disk, trigger onChange
- this.onChange(content)
- }
- }
- )
- }
- }
-
- close() {
- if (this.watcher) {
- this.watcher.close()
- this.watcher = null
- }
- }
-}
-
-
-// Buffer
-let buffers = {}
-export function loadBuffer(path) {
- if (buffers[path]) {
- buffers[path].close()
- }
- buffers[path] = new Buffer({
- filePath: getFullBufferFilePath(path),
- onChange: (content) => {
- console.log("Old buffer.js onChange")
- win?.webContents.send("buffer-content:change", path, content)
- },
- })
- return buffers[path]
-}
-
-ipcMain.handle('buffer-content:load', async (event, path) => {
- if (!buffers[path]) {
- loadBuffer(path)
- }
- if (buffers[path].exists() && !(eraseInitialContent && isDev)) {
- return await buffers[path].load()
- } else {
- return isDev ? initialDevContent : initialContent
- }
-});
-
-async function save(path, content) {
- return await buffers[path].save(content)
-}
-
-ipcMain.handle('buffer-content:save', async (event, path, content) => {
- return await save(path, content)
-});
-
-export let contentSaved = false
-ipcMain.handle('buffer-content:saveAndQuit', async (event, contents) => {
- for (const [path, content] of contents) {
- await save(path, content)
- }
- contentSaved = true
- app.quit()
-})
-
-ipcMain.handle("buffer-content:selectLocation", async () => {
- let result = await dialog.showOpenDialog({
- title: "Select directory to store buffer",
- properties: [
- "openDirectory",
- "createDirectory",
- "noResolveAliases",
- ],
- })
- if (result.canceled) {
- return
- }
- const filePath = result.filePaths[0]
- if (fs.existsSync(constructBufferFilePath(filePath))) {
- if (dialog.showMessageBoxSync({
- type: "question",
- message: "The selected directory already contains a buffer file. It will be loaded. Do you want to continue?",
- buttons: ["Cancel", "Continue"],
- }) === 0) {
- return
- }
- }
- return filePath
-})
diff --git a/electron/main/file-library.js b/electron/main/file-library.js
index bc06a18..6b3649d 100644
--- a/electron/main/file-library.js
+++ b/electron/main/file-library.js
@@ -5,6 +5,16 @@ import { join, dirname, basename } from "path"
import * as jetpack from "fs-jetpack";
import { app, ipcMain, dialog } from "electron"
+import CONFIG from "../config"
+import { SCRATCH_FILE_NAME } from "../../src/common/constants"
+import { NoteFormat } from "../../src/common/note-format"
+import { isDev } from '../detect-platform';
+import { initialContent, initialDevContent } from '../initial-content'
+
+export const NOTES_DIR_NAME = isDev ? "notes-dev" : "notes"
+
+
+let library
const untildify = (pathWithTilde) => {
const homeDir = os.homedir()
@@ -42,6 +52,11 @@ export class FileLibrary {
this.watcher = null;
this.contentSaved = false
this.onChangeCallback = null
+
+ // create scratch.txt if it doesn't exist
+ if (!this.jetpack.exists(SCRATCH_FILE_NAME)) {
+ this.jetpack.write(SCRATCH_FILE_NAME, isDev ? initialDevContent : initialContent)
+ }
}
async exists(path) {
@@ -82,7 +97,7 @@ export class FileLibrary {
}
async getList() {
- console.log("Loading notes")
+ //console.log("Listing notes")
const notes = {}
const files = await this.jetpack.findAsync(".", {
matching: "*.txt",
@@ -199,10 +214,13 @@ export class NoteBuffer {
}
}
+export function setCurrentFileLibrary(lib) {
+ library = lib
+}
-export function setupFileLibraryEventHandlers(library, win) {
+export function setupFileLibraryEventHandlers(win) {
ipcMain.handle('buffer:load', async (event, path) => {
- console.log("buffer:load", path)
+ //console.log("buffer:load", path)
return await library.load(path)
});
@@ -244,5 +262,71 @@ export function setupFileLibraryEventHandlers(library, win) {
return await library.move(path, newPath)
});
- library.setupWatcher(win)
+ ipcMain.handle("library:selectLocation", async () => {
+ let result = await dialog.showOpenDialog({
+ title: "Select directory to store buffer",
+ properties: [
+ "openDirectory",
+ "createDirectory",
+ "noResolveAliases",
+ ],
+ })
+ if (result.canceled) {
+ return
+ }
+ const filePath = result.filePaths[0]
+ return filePath
+ })
}
+
+
+export async function migrateBufferFileToLibrary(app) {
+ async function ensureBufferFileMetadata(filePath) {
+ const metadata = await readNoteMetadata(filePath)
+ //console.log("Metadata", metadata)
+ if (!metadata || !metadata.name) {
+ console.log("Adding metadata to", filePath)
+ const note = NoteFormat.load(jetpack.read(filePath))
+ note.metadata.name = "Scratch"
+ jetpack.write(filePath, note.serialize())
+ } else {
+ console.log("Metadata already exists for", filePath)
+ }
+ }
+
+ const defaultLibraryPath = join(app.getPath("userData"), NOTES_DIR_NAME)
+ const customBufferPath = CONFIG.get("settings.bufferPath")
+ const oldBufferFile = isDev ? "buffer-dev.txt" : "buffer.txt"
+ if (customBufferPath) {
+ // if the new buffer file exists, no need to migrate
+ if (jetpack.exists(join(customBufferPath, SCRATCH_FILE_NAME)) === "file") {
+ return
+ }
+ const oldBufferFileFullPath = join(customBufferPath, oldBufferFile)
+ if (jetpack.exists(oldBufferFileFullPath) === "file") {
+ const newFileFullPath = join(customBufferPath, SCRATCH_FILE_NAME);
+ console.log(`Migrating file ${oldBufferFileFullPath} to ${newFileFullPath}`)
+ // rename buffer file to scratch.txt
+ jetpack.move(oldBufferFileFullPath, newFileFullPath)
+ // add metadata to scratch.txt (just to be sure, we'll double check that it's needed first)
+ await ensureBufferFileMetadata(newFileFullPath)
+ }
+ } else {
+ // if the new buffer file exists, no need to migrate
+ if (jetpack.exists(join(defaultLibraryPath, SCRATCH_FILE_NAME)) === "file") {
+ return
+ }
+ // check if the old buffer file exists, while the default *library* path doesn't exist
+ const oldBufferFileFullPath = join(app.getPath("userData"), oldBufferFile)
+ if (jetpack.exists(oldBufferFileFullPath) === "file" && jetpack.exists(defaultLibraryPath) !== "dir") {
+ const newFileFullPath = join(defaultLibraryPath, SCRATCH_FILE_NAME);
+ console.log(`Migrating buffer file ${oldBufferFileFullPath} to ${newFileFullPath}`)
+ // create the default library path
+ jetpack.dir(defaultLibraryPath)
+ // move the buffer file to the library path
+ jetpack.move(oldBufferFileFullPath, newFileFullPath)
+ // add metadata to scratch.txt
+ await ensureBufferFileMetadata(newFileFullPath)
+ }
+ }
+}
\ No newline at end of file
diff --git a/electron/main/index.ts b/electron/main/index.ts
index 93a24f5..c78a57f 100644
--- a/electron/main/index.ts
+++ b/electron/main/index.ts
@@ -9,8 +9,13 @@ import CONFIG from "../config"
import { isDev, isLinux, isMac, isWindows } from '../detect-platform';
import { initializeAutoUpdate, checkForUpdates } from './auto-update';
import { fixElectronCors } from './cors';
-import { loadBuffer, contentSaved } from './buffer';
-import { FileLibrary, setupFileLibraryEventHandlers } from './file-library';
+import {
+ FileLibrary,
+ setupFileLibraryEventHandlers,
+ setCurrentFileLibrary,
+ migrateBufferFileToLibrary,
+ NOTES_DIR_NAME
+} from './file-library';
// The built directory structure
@@ -310,7 +315,9 @@ function registerAlwaysOnTop() {
}
app.whenReady().then(createWindow).then(async () => {
- setupFileLibraryEventHandlers(fileLibrary, win)
+ initFileLibrary(win).then(() => {
+ setupFileLibraryEventHandlers(win)
+ })
initializeAutoUpdate(win)
registerGlobalHotkey()
registerShowInDock()
@@ -352,14 +359,28 @@ ipcMain.handle('dark-mode:set', (event, mode) => {
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
+
// Initialize note/file library
-const customLibraryPath = CONFIG.get("settings.bufferPath")
-const libraryPath = customLibraryPath ? customLibraryPath : join(app.getPath("userData"), "notes")
-console.log("libraryPath", libraryPath)
-try {
- fileLibrary = new FileLibrary(libraryPath)
-} catch (error) {
- initErrors.push(`Error: ${error.message}`)
+async function initFileLibrary(win) {
+ await migrateBufferFileToLibrary(app)
+
+ const customLibraryPath = CONFIG.get("settings.bufferPath")
+ const defaultLibraryPath = join(app.getPath("userData"), NOTES_DIR_NAME)
+ const libraryPath = customLibraryPath ? customLibraryPath : defaultLibraryPath
+ //console.log("libraryPath", libraryPath)
+
+ // if we're using the default library path, and it doesn't exist (e.g. first time run), create it
+ if (!customLibraryPath && !fs.existsSync(defaultLibraryPath)) {
+ fs.mkdirSync(defaultLibraryPath)
+ }
+
+ try {
+ fileLibrary = new FileLibrary(libraryPath)
+ fileLibrary.setupWatcher(win)
+ } catch (error) {
+ initErrors.push(`Error: ${error.message}`)
+ }
+ setCurrentFileLibrary(fileLibrary)
}
ipcMain.handle("getInitErrors", () => {
@@ -393,9 +414,10 @@ ipcMain.handle('settings:set', async (event, settings) => {
registerAlwaysOnTop()
}
if (bufferPathChanged) {
- const buffer = loadBuffer()
- if (buffer.exists()) {
- win?.webContents.send("buffer-content:change", await buffer.load())
- }
+ console.log("bufferPath changed, closing existing file library")
+ fileLibrary.close()
+ console.log("initializing new file library")
+ initFileLibrary(win)
+ await win.webContents.send("library:pathChanged")
}
})
diff --git a/electron/preload/index.ts b/electron/preload/index.ts
index bb1e553..9e5da29 100644
--- a/electron/preload/index.ts
+++ b/electron/preload/index.ts
@@ -108,12 +108,11 @@ contextBridge.exposeInMainWorld("heynote", {
},
async selectLocation() {
- return await ipcRenderer.invoke("buffer-content:selectLocation")
+ return await ipcRenderer.invoke("library:selectLocation")
},
- callbacks(callbacks) {
- ipcRenderer.on("buffer:noteMetadataChanged", (event, path, info) => callbacks?.noteMetadataChanged(path, info))
- ipcRenderer.on("buffer:noteRemoved", (event, path) => callbacks?.noteRemoved(path))
+ setLibraryPathChangeCallback(callback) {
+ ipcRenderer.on("library:pathChanged", callback)
},
},
diff --git a/shared-utils/key-helper.ts b/shared-utils/key-helper.ts
index 9930042..900a78f 100644
--- a/shared-utils/key-helper.ts
+++ b/shared-utils/key-helper.ts
@@ -9,6 +9,8 @@ export const keyHelpStr = (platform: string) => {
[`${altChar} + Shift + Enter`, "Add new block at the start of the buffer"],
[`${modChar} + ${altChar} + Enter`, "Split the current block at cursor position"],
[`${modChar} + L`, "Change block language"],
+ [`${modChar} + S`, "Create a new note from the current block"],
+ [`${modChar} + P`, "Open note selector"],
[`${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 buffer"],
diff --git a/src/common/constants.js b/src/common/constants.js
new file mode 100644
index 0000000..42c1766
--- /dev/null
+++ b/src/common/constants.js
@@ -0,0 +1 @@
+export const SCRATCH_FILE_NAME = "scratch.txt"
diff --git a/src/editor/note-format.js b/src/common/note-format.js
similarity index 100%
rename from src/editor/note-format.js
rename to src/common/note-format.js
diff --git a/src/components/App.vue b/src/components/App.vue
index 982b80a..b9683b0 100644
--- a/src/components/App.vue
+++ b/src/components/App.vue
@@ -137,6 +137,11 @@
this.$refs.editor.focus()
},
+ setTheme(theme) {
+ window.heynote.themeMode.set(theme)
+ this.themeSetting = theme
+ },
+
onSelectLanguage(language) {
this.closeDialog()
this.$refs.editor.setLanguage(language)
@@ -170,11 +175,8 @@
ref="editor"
/>