Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { resolve } from 'node:path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import type { Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import { harperWasmAsset } from '../../tooling/vite/harper-wasm-asset'

const INTERNAL_WORKSPACE_PACKAGES = [
'@zennotes/app-core',
Expand Down Expand Up @@ -250,12 +251,15 @@ export default defineConfig({
renderer: {
root: resolve(__dirname, 'src/renderer'),
// Typst ships a WASM compiler loaded lazily via `?url` + dynamic import; keep
// it out of the esbuild dep pre-bundler so the wasm glue stays intact.
// it out of the esbuild dep pre-bundler so the wasm glue stays intact. The
// same goes for Harper, whose worker is an inline blob the pre-bundler
// would otherwise rewrite.
optimizeDeps: {
exclude: [
'@myriaddreamin/typst.ts',
'@myriaddreamin/typst-ts-web-compiler',
'@myriaddreamin/typst-ts-renderer'
'@myriaddreamin/typst-ts-renderer',
'harper.js'
]
},
build: {
Expand All @@ -281,6 +285,6 @@ export default defineConfig({
'@bridge-contract': resolve(__dirname, '../../packages/bridge-contract/src')
}
},
plugins: [onigurumaDataUrl(), react()]
plugins: [onigurumaDataUrl(), harperWasmAsset(), react()]
}
})
4 changes: 3 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/desktop",
"productName": "ZenNotes",
"version": "2.43.0",
"version": "2.44.0",
"description": "ZenNotes desktop shell",
"private": true,
"main": "./out/main/index.js",
Expand Down Expand Up @@ -42,6 +42,7 @@
"@codemirror/lang-markdown": "^6.3.1",
"@codemirror/language": "^6.10.6",
"@codemirror/language-data": "^6.5.1",
"@codemirror/lint": "^6.9.7",
"@codemirror/search": "^6.5.8",
"@codemirror/state": "^6.5.0",
"@codemirror/view": "^6.35.3",
Expand All @@ -63,6 +64,7 @@
"font-list": "^2.0.2",
"function-plot": "^1.25.3",
"gray-matter": "^4.0.3",
"harper.js": "^2.7.0",
"highlight.js": "^11.10.0",
"jsxgraph": "^1.12.2",
"katex": "^0.16.15",
Expand Down
10 changes: 10 additions & 0 deletions apps/desktop/src/main/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ const SCALAR_FIELDS: Partial<Record<PortablePrefKey, ScalarFieldMap>> = {
tomlKey: 'typst_tag_preambles',
comment: 'true | false — prepend Typst definitions from notes in a `typst` folder, chosen by a note\'s tags'
},
harperEnabled: {
section: 'editor',
tomlKey: 'harper_enabled',
comment: 'true | false: grammar and spelling with Harper, checked on this device'
},
harperDialect: {
section: 'editor',
tomlKey: 'harper_dialect',
comment: 'american | british | australian | canadian | indian'
},
looseMathDelimiters: {
section: 'editor',
tomlKey: 'loose_math_delimiters',
Expand Down
47 changes: 47 additions & 0 deletions apps/desktop/src/main/atomic-write.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { promises as fs } from 'node:fs'
import path from 'node:path'

const ATOMIC_RENAME_ATTEMPTS = 20

function transientRenameError(error: unknown): boolean {
const code = (error as NodeJS.ErrnoException | null)?.code
return code === 'EACCES' || code === 'EPERM' || code === 'EBUSY'
}

/** Wait out a reader that temporarily denies replacing the destination. */
export async function renameWithRetry(
from: string,
to: string,
rename: (from: string, to: string) => Promise<void> = fs.rename,
pause: (delayMs: number) => Promise<void> = (delayMs) =>
new Promise<void>((resolve) => setTimeout(resolve, delayMs))
): Promise<void> {
for (let attempt = 1; ; attempt++) {
try {
await rename(from, to)
return
} catch (error) {
if (attempt >= ATOMIC_RENAME_ATTEMPTS || !transientRenameError(error)) throw error
await pause(Math.min(2 ** (attempt - 1), 25))
}
}
}

/** Follow a symlink to the file it points at, so an atomic write lands on the
* target instead of replacing the link. A dangling link resolves to the path
* it names, which is where a plain write would have created the file. */
export async function atomicWriteTarget(absPath: string): Promise<string> {
let stats
try {
stats = await fs.lstat(absPath)
} catch {
return absPath
}
if (!stats.isSymbolicLink()) return absPath
try {
return await fs.realpath(absPath)
} catch {
return path.resolve(path.dirname(absPath), await fs.readlink(absPath))
}
}

Loading
Loading