latex-stickies 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/bin/latex-stickies.js +36 -0
  4. package/build/icon.icns +0 -0
  5. package/build/icon.png +0 -0
  6. package/package.json +76 -0
  7. package/src/main.js +308 -0
  8. package/src/preload.js +27 -0
  9. package/src/renderer/editor.js +201 -0
  10. package/src/renderer/markdown.js +154 -0
  11. package/src/renderer/note.css +288 -0
  12. package/src/renderer/note.html +34 -0
  13. package/src/renderer/note.js +210 -0
  14. package/src/renderer/vendor/fonts/KaTeX_AMS-Regular.ttf +0 -0
  15. package/src/renderer/vendor/fonts/KaTeX_AMS-Regular.woff +0 -0
  16. package/src/renderer/vendor/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  17. package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  18. package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  19. package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  20. package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  21. package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  22. package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  23. package/src/renderer/vendor/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  24. package/src/renderer/vendor/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  25. package/src/renderer/vendor/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  26. package/src/renderer/vendor/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  27. package/src/renderer/vendor/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  28. package/src/renderer/vendor/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  29. package/src/renderer/vendor/fonts/KaTeX_Main-Bold.ttf +0 -0
  30. package/src/renderer/vendor/fonts/KaTeX_Main-Bold.woff +0 -0
  31. package/src/renderer/vendor/fonts/KaTeX_Main-Bold.woff2 +0 -0
  32. package/src/renderer/vendor/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  33. package/src/renderer/vendor/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  34. package/src/renderer/vendor/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  35. package/src/renderer/vendor/fonts/KaTeX_Main-Italic.ttf +0 -0
  36. package/src/renderer/vendor/fonts/KaTeX_Main-Italic.woff +0 -0
  37. package/src/renderer/vendor/fonts/KaTeX_Main-Italic.woff2 +0 -0
  38. package/src/renderer/vendor/fonts/KaTeX_Main-Regular.ttf +0 -0
  39. package/src/renderer/vendor/fonts/KaTeX_Main-Regular.woff +0 -0
  40. package/src/renderer/vendor/fonts/KaTeX_Main-Regular.woff2 +0 -0
  41. package/src/renderer/vendor/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  42. package/src/renderer/vendor/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  43. package/src/renderer/vendor/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  44. package/src/renderer/vendor/fonts/KaTeX_Math-Italic.ttf +0 -0
  45. package/src/renderer/vendor/fonts/KaTeX_Math-Italic.woff +0 -0
  46. package/src/renderer/vendor/fonts/KaTeX_Math-Italic.woff2 +0 -0
  47. package/src/renderer/vendor/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  48. package/src/renderer/vendor/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  49. package/src/renderer/vendor/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  50. package/src/renderer/vendor/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  51. package/src/renderer/vendor/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  52. package/src/renderer/vendor/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  53. package/src/renderer/vendor/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  54. package/src/renderer/vendor/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  55. package/src/renderer/vendor/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  56. package/src/renderer/vendor/fonts/KaTeX_Script-Regular.ttf +0 -0
  57. package/src/renderer/vendor/fonts/KaTeX_Script-Regular.woff +0 -0
  58. package/src/renderer/vendor/fonts/KaTeX_Script-Regular.woff2 +0 -0
  59. package/src/renderer/vendor/fonts/KaTeX_Size1-Regular.ttf +0 -0
  60. package/src/renderer/vendor/fonts/KaTeX_Size1-Regular.woff +0 -0
  61. package/src/renderer/vendor/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  62. package/src/renderer/vendor/fonts/KaTeX_Size2-Regular.ttf +0 -0
  63. package/src/renderer/vendor/fonts/KaTeX_Size2-Regular.woff +0 -0
  64. package/src/renderer/vendor/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  65. package/src/renderer/vendor/fonts/KaTeX_Size3-Regular.ttf +0 -0
  66. package/src/renderer/vendor/fonts/KaTeX_Size3-Regular.woff +0 -0
  67. package/src/renderer/vendor/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  68. package/src/renderer/vendor/fonts/KaTeX_Size4-Regular.ttf +0 -0
  69. package/src/renderer/vendor/fonts/KaTeX_Size4-Regular.woff +0 -0
  70. package/src/renderer/vendor/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  71. package/src/renderer/vendor/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  72. package/src/renderer/vendor/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  73. package/src/renderer/vendor/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  74. package/src/renderer/vendor/katex.min.css +1 -0
  75. package/src/renderer/vendor/katex.min.js +1 -0
  76. package/src/renderer/vendor/marked.js +80 -0
  77. package/src/renderer/vendor/prism.js +2009 -0
  78. package/src/renderer/vendor/purify.js +3 -0
  79. package/src/store.js +116 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin Jusak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # LaTeX Stickies
2
+
3
+ Sticky notes for your desktop that render LaTeX and Markdown.
4
+
5
+ Write `$e^{i\pi} + 1 = 0$` in a note and it renders as soon as you click away.
6
+ Notes stay where you put them, in the color you chose, across restarts.
7
+
8
+ ```
9
+ npx latex-stickies
10
+ ```
11
+
12
+ Or install it so it stays around:
13
+
14
+ ```
15
+ npm install -g latex-stickies
16
+ latex-stickies
17
+ ```
18
+
19
+ Requires Node 22.12 or newer. The first run downloads the Electron runtime (~230 MB), so
20
+ give it a minute; later launches are instant.
21
+
22
+ ## Writing in a note
23
+
24
+ Click a note to edit it, click away to render. `Esc` also leaves edit mode.
25
+
26
+ **Math** — `$…$` inline, `$$…$$` on its own line. Delimiters have to hug their
27
+ content, so ordinary prose like `costs $5 and $6` stays text rather than turning
28
+ into a formula. Broken TeX shows the source underlined in red with the parse
29
+ error on hover, instead of blanking the note.
30
+
31
+ **Markdown** — headings, `**bold**`, `*italic*`, `~~strikethrough~~`, lists,
32
+ task lists, tables, blockquotes, and links (which open in your browser).
33
+
34
+ **Code** — fenced blocks are highlighted in 20 languages, with the language
35
+ labelled and a copy button on hover:
36
+
37
+ ````
38
+ ```python
39
+ def gaussian(x, mu, sigma):
40
+ return exp(-((x - mu) ** 2) / (2 * sigma ** 2))
41
+ ```
42
+ ````
43
+
44
+ Aliases work as you would expect: `sh`, `shell` and `zsh` all mean bash, `py`
45
+ means python, and `tex` means latex. A fence with no language stays plain --
46
+ guessing on a two-line snippet is wrong often enough to be worse than nothing.
47
+
48
+ Inside a fence the editor behaves like a code editor: Enter keeps your
49
+ indentation and adds a level after `:` or `{`, Tab and Shift+Tab indent and
50
+ dedent (including a whole selection), and brackets and quotes auto-close.
51
+ Outside a fence it behaves like a notes field instead -- lists continue on
52
+ Enter, and quotes are left alone so `don't` types normally.
53
+
54
+ ## Keyboard
55
+
56
+ Use `Ctrl` in place of `Cmd` on Windows and Linux.
57
+
58
+ | | |
59
+ |---|---|
60
+ | `Cmd+N` | New note |
61
+ | `Cmd+W` | Close note |
62
+ | `Cmd+Backspace` | Delete note |
63
+ | `Cmd+E` | Toggle edit / preview |
64
+ | `Cmd+T` | Keep on top |
65
+ | `Cmd+±` | Text size |
66
+ | `Esc` | Leave edit mode |
67
+
68
+ ## Where notes live
69
+
70
+ | | |
71
+ |---|---|
72
+ | macOS | `~/Library/Application Support/latex-stickies/notes.json` |
73
+ | Windows | `%APPDATA%\latex-stickies\notes.json` |
74
+ | Linux | `~/.config/latex-stickies/notes.json` |
75
+
76
+ Saves are atomic -- written to a temp file, flushed, then renamed over the
77
+ target -- so a crash, a force quit, or pulling the power leaves the previous
78
+ good file intact rather than a truncated one. Only one instance runs at a time,
79
+ because two copies would hold divergent notes in memory and the last to quit
80
+ would silently overwrite the other.
81
+
82
+ ## Development
83
+
84
+ ```
85
+ npm install
86
+ npm start # run from source
87
+ npm test # 4 suites, 62 assertions
88
+ npm run vendor # regenerate the bundled Prism
89
+ npm run icon # rebuild the app icon from assets/icon-master.png
90
+ ```
91
+
92
+ Artwork lives in `assets/`: `logo.jpeg` is the original, and `icon-master.png`
93
+ is the prepared 1024px master the icon is generated from -- transparent ground,
94
+ art filling 824 of the canvas, which is the macOS convention that keeps an icon
95
+ at the same visual weight as the rest of the Dock.
96
+
97
+ The renderer loads over `file://` under a strict content-security policy, so
98
+ KaTeX, marked, DOMPurify and Prism are vendored into `src/renderer/vendor/`
99
+ rather than pulled from a CDN. Note text is sanitized before it reaches the
100
+ page, and the renderer talks to disk only through a six-function preload bridge.
101
+
102
+ To build a macOS `.app` and `.dmg`:
103
+
104
+ ```
105
+ npm run dist
106
+ ```
107
+
108
+ The build is unsigned, so macOS will warn on other machines unless you sign it
109
+ with an Apple Developer ID.
110
+
111
+ ## License
112
+
113
+ MIT
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Launcher for `npx latex-stickies`.
4
+ *
5
+ * The `electron` package exports the path to the platform's Electron binary.
6
+ * We spawn it against this package's own directory, detached, so the notes
7
+ * keep running after the terminal that started them is closed -- a sticky
8
+ * note that dies with your shell is not a sticky note.
9
+ */
10
+ const { spawn } = require('child_process');
11
+ const path = require('path');
12
+
13
+ let electron;
14
+ try {
15
+ electron = require('electron');
16
+ } catch (_) {
17
+ console.error(
18
+ 'Could not find the Electron runtime.\n' +
19
+ 'Reinstall with: npm install -g latex-stickies'
20
+ );
21
+ process.exit(1);
22
+ }
23
+
24
+ const appDir = path.join(__dirname, '..');
25
+ const child = spawn(electron, [appDir, ...process.argv.slice(2)], {
26
+ detached: true,
27
+ stdio: 'ignore',
28
+ });
29
+
30
+ child.on('error', (err) => {
31
+ console.error('Failed to launch LaTeX Stickies:', err.message);
32
+ process.exit(1);
33
+ });
34
+
35
+ child.unref();
36
+ console.log('LaTeX Stickies is running. Close the notes to quit.');
Binary file
package/build/icon.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "latex-stickies",
3
+ "version": "1.0.0",
4
+ "description": "Sticky notes for your desktop that render LaTeX and Markdown.",
5
+ "main": "src/main.js",
6
+ "scripts": {
7
+ "start": "electron .",
8
+ "test": "node scripts/test.js",
9
+ "vendor": "node scripts/vendor.js",
10
+ "icon": "node scripts/make-icon.js",
11
+ "dist": "node scripts/build-dmg.js",
12
+ "install-app": "npm run dist && rm -rf \"/Applications/LaTeX Stickies.app\" && cp -R \"dist/mac-arm64/LaTeX Stickies.app\" /Applications/ && xattr -dr com.apple.quarantine \"/Applications/LaTeX Stickies.app\"",
13
+ "prepublishOnly": "npm test"
14
+ },
15
+ "build": {
16
+ "appId": "com.kevinjusak.latexsticky",
17
+ "productName": "LaTeX Stickies",
18
+ "files": [
19
+ "src/**/*",
20
+ "package.json"
21
+ ],
22
+ "mac": {
23
+ "category": "public.app-category.productivity",
24
+ "target": [
25
+ "dmg",
26
+ "zip"
27
+ ],
28
+ "icon": "build/icon.icns"
29
+ }
30
+ },
31
+ "devDependencies": {
32
+ "dompurify": "^3.1.6",
33
+ "electron-builder": "^26.15.3",
34
+ "jsdom": "^30.0.1",
35
+ "katex": "^0.16.11",
36
+ "marked": "^18.0.11",
37
+ "prismjs": "^1.30.0"
38
+ },
39
+ "dependencies": {
40
+ "electron": "^44.1.1"
41
+ },
42
+ "allowScripts": {
43
+ "electron@44.1.1": true
44
+ },
45
+ "author": "Kevin Jusak",
46
+ "license": "MIT",
47
+ "keywords": [
48
+ "latex",
49
+ "katex",
50
+ "markdown",
51
+ "sticky-notes",
52
+ "notes",
53
+ "electron",
54
+ "math"
55
+ ],
56
+ "bin": {
57
+ "latex-stickies": "bin/latex-stickies.js"
58
+ },
59
+ "files": [
60
+ "src/",
61
+ "bin/",
62
+ "build/icon.icns",
63
+ "build/icon.png"
64
+ ],
65
+ "engines": {
66
+ "node": ">=22.12.0"
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/kev-nj/latex-stickies.git"
71
+ },
72
+ "homepage": "https://github.com/kev-nj/latex-stickies#readme",
73
+ "bugs": {
74
+ "url": "https://github.com/kev-nj/latex-stickies/issues"
75
+ }
76
+ }
package/src/main.js ADDED
@@ -0,0 +1,308 @@
1
+ const { app, BrowserWindow, ipcMain, Menu, screen, shell } = require('electron');
2
+ const path = require('path');
3
+
4
+ // Pin the identity before anything derives a path from it.
5
+ //
6
+ // Electron builds userData out of the app name, so the notes directory would
7
+ // otherwise follow whatever `name` happens to be in package.json. Renaming the
8
+ // package for npm silently moved every note to a new folder and greeted the
9
+ // user with an empty desk. Fixing the directory here decouples where notes live
10
+ // from what the package is called.
11
+ app.setName('LaTeX Stickies');
12
+ app.setPath('userData', path.join(app.getPath('appData'), 'latex-stickies'));
13
+
14
+ // Required after the path is set: store.js resolves notes.json when it loads.
15
+ const store = require('./store');
16
+
17
+ const ICON = path.join(__dirname, '..', 'build', 'icon.png');
18
+
19
+ const COLORS = ['yellow', 'blue', 'green', 'pink', 'purple', 'gray'];
20
+
21
+ /** noteId -> BrowserWindow */
22
+ const windows = new Map();
23
+
24
+ function randomId() {
25
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
26
+ }
27
+
28
+ function cascadeBounds() {
29
+ const area = screen.getPrimaryDisplay().workArea;
30
+ const n = windows.size;
31
+ return {
32
+ x: area.x + 60 + ((n * 28) % 320),
33
+ y: area.y + 60 + ((n * 28) % 320),
34
+ width: 340,
35
+ height: 380,
36
+ };
37
+ }
38
+
39
+ function openExternal(url) {
40
+ // Only real web links; never file:// or custom schemes typed into a note.
41
+ if (/^https?:\/\//i.test(url)) shell.openExternal(url);
42
+ }
43
+
44
+ /**
45
+ * Keeps a note reachable. Bounds are restored verbatim from disk, so a note
46
+ * last placed on a monitor that is no longer attached would open completely
47
+ * off-screen -- present in the file, invisible on the desk, and indistinguishable
48
+ * from a lost note. Fall back to a fresh position when the saved rectangle does
49
+ * not overlap any current display.
50
+ */
51
+ function onScreenBounds(bounds) {
52
+ if (!bounds || typeof bounds.x !== 'number' || typeof bounds.y !== 'number') {
53
+ return cascadeBounds();
54
+ }
55
+ const visible = screen.getAllDisplays().some(({ workArea: a }) =>
56
+ bounds.x < a.x + a.width &&
57
+ bounds.x + (bounds.width || 0) > a.x &&
58
+ bounds.y < a.y + a.height &&
59
+ bounds.y + (bounds.height || 0) > a.y);
60
+ return visible ? bounds : { ...cascadeBounds(), width: bounds.width, height: bounds.height };
61
+ }
62
+
63
+ function openNote(note) {
64
+ const existing = windows.get(note.id);
65
+ if (existing) {
66
+ existing.show();
67
+ existing.focus();
68
+ return existing;
69
+ }
70
+
71
+ const win = new BrowserWindow({
72
+ ...onScreenBounds(note.bounds),
73
+ minWidth: 220,
74
+ minHeight: 160,
75
+ frame: false,
76
+ transparent: true,
77
+ icon: ICON,
78
+ hasShadow: true,
79
+ alwaysOnTop: !!note.alwaysOnTop,
80
+ skipTaskbar: false,
81
+ webPreferences: {
82
+ preload: path.join(__dirname, 'preload.js'),
83
+ contextIsolation: true,
84
+ nodeIntegration: false,
85
+ additionalArguments: [`--note-id=${note.id}`],
86
+ },
87
+ });
88
+
89
+ win.loadFile(path.join(__dirname, 'renderer', 'note.html'));
90
+
91
+ const persistBounds = () => {
92
+ if (win.isDestroyed() || win.isMinimized()) return;
93
+ store.upsert({ id: note.id, bounds: win.getBounds() });
94
+ };
95
+ win.on('resize', persistBounds);
96
+ win.on('move', persistBounds);
97
+ win.on('closed', () => windows.delete(note.id));
98
+
99
+ // Links inside a note open in the real browser, never inside the sticky.
100
+ win.webContents.setWindowOpenHandler(({ url }) => {
101
+ openExternal(url);
102
+ return { action: 'deny' };
103
+ });
104
+
105
+ // A note must never navigate away from its own page and become a browser.
106
+ win.webContents.on('will-navigate', (event, url) => {
107
+ event.preventDefault();
108
+ openExternal(url);
109
+ });
110
+
111
+ windows.set(note.id, win);
112
+ return win;
113
+ }
114
+
115
+ function createNote(seed = {}) {
116
+ const note = {
117
+ id: randomId(),
118
+ body: '',
119
+ color: seed.color || COLORS[Math.floor(Math.random() * COLORS.length)],
120
+ fontSize: 15,
121
+ alwaysOnTop: false,
122
+ bounds: cascadeBounds(),
123
+ createdAt: Date.now(),
124
+ ...seed,
125
+ };
126
+ store.upsert(note);
127
+ openNote(note);
128
+ return note;
129
+ }
130
+
131
+ function restoreNotes() {
132
+ const notes = store.all();
133
+ if (notes.length === 0) {
134
+ createNote({ body: WELCOME });
135
+ return;
136
+ }
137
+ notes.forEach(openNote);
138
+ }
139
+
140
+ const WELCOME = [
141
+ '# Welcome to LaTeX Stickies',
142
+ '',
143
+ 'Type math inline like $e^{i\\pi} + 1 = 0$, or on its own line:',
144
+ '',
145
+ '$$\\int_{-\\infty}^{\\infty} e^{-x^2}\\,dx = \\sqrt{\\pi}$$',
146
+ '',
147
+ 'Markdown works too:',
148
+ '',
149
+ '- **bold**, *italic*, `code`, ~~strikethrough~~',
150
+ '- [ ] task lists',
151
+ '- > blockquotes, tables, and fenced code blocks',
152
+ '',
153
+ 'Click the note to edit, click outside to render.',
154
+ 'Cmd+N new note · Cmd+E toggle edit · Cmd+T always on top',
155
+ ].join('\n');
156
+
157
+ function buildMenu() {
158
+ const template = [
159
+ {
160
+ label: app.name,
161
+ submenu: [
162
+ { role: 'about' },
163
+ { type: 'separator' },
164
+ { role: 'hide' },
165
+ { role: 'hideOthers' },
166
+ { type: 'separator' },
167
+ { role: 'quit' },
168
+ ],
169
+ },
170
+ {
171
+ label: 'File',
172
+ submenu: [
173
+ { label: 'New Note', accelerator: 'CmdOrCtrl+N', click: () => createNote() },
174
+ {
175
+ label: 'Close Note',
176
+ accelerator: 'CmdOrCtrl+W',
177
+ click: () => BrowserWindow.getFocusedWindow()?.close(),
178
+ },
179
+ { type: 'separator' },
180
+ {
181
+ label: 'Delete Note',
182
+ accelerator: 'CmdOrCtrl+Backspace',
183
+ click: () => {
184
+ const win = BrowserWindow.getFocusedWindow();
185
+ if (win) win.webContents.send('request-delete');
186
+ },
187
+ },
188
+ ],
189
+ },
190
+ { role: 'editMenu' },
191
+ {
192
+ label: 'Note',
193
+ submenu: [
194
+ {
195
+ label: 'Toggle Edit / Preview',
196
+ accelerator: 'CmdOrCtrl+E',
197
+ click: () => BrowserWindow.getFocusedWindow()?.webContents.send('toggle-edit'),
198
+ },
199
+ {
200
+ label: 'Always on Top',
201
+ accelerator: 'CmdOrCtrl+T',
202
+ click: () => {
203
+ const win = BrowserWindow.getFocusedWindow();
204
+ if (!win) return;
205
+ const next = !win.isAlwaysOnTop();
206
+ win.setAlwaysOnTop(next);
207
+ win.webContents.send('always-on-top-changed', next);
208
+ },
209
+ },
210
+ { type: 'separator' },
211
+ {
212
+ label: 'Bigger Text',
213
+ accelerator: 'CmdOrCtrl+Plus',
214
+ click: () => BrowserWindow.getFocusedWindow()?.webContents.send('font-size', 1),
215
+ },
216
+ {
217
+ label: 'Smaller Text',
218
+ accelerator: 'CmdOrCtrl+-',
219
+ click: () => BrowserWindow.getFocusedWindow()?.webContents.send('font-size', -1),
220
+ },
221
+ ],
222
+ },
223
+ {
224
+ label: 'Window',
225
+ submenu: [
226
+ { role: 'minimize' },
227
+ {
228
+ label: 'Bring All Notes to Front',
229
+ click: () => windows.forEach((w) => w.show()),
230
+ },
231
+ { type: 'separator' },
232
+ { role: 'toggleDevTools' },
233
+ ],
234
+ },
235
+ ];
236
+ Menu.setApplicationMenu(Menu.buildFromTemplate(template));
237
+ }
238
+
239
+ // Every instance reads and writes the same notes.json, so a second one is not
240
+ // just clutter -- two copies hold divergent state in memory and the last to
241
+ // save wins, silently losing notes. Refuse to start twice; surface the notes
242
+ // that are already running instead.
243
+ if (!app.requestSingleInstanceLock()) {
244
+ // Leave before any window or handler is set up. Relying on quit() alone to
245
+ // unwind a half-initialised second instance is how two processes end up
246
+ // touching notes.json at once.
247
+ app.exit(0);
248
+ }
249
+
250
+ app.on('second-instance', () => {
251
+ windows.forEach((win) => {
252
+ if (win.isMinimized()) win.restore();
253
+ win.show();
254
+ });
255
+ });
256
+
257
+ app.whenReady().then(() => {
258
+ // Run from npm there is no .app bundle to carry the icon, so macOS would show
259
+ // the generic Electron atom in the Dock. Set it explicitly.
260
+ if (process.platform === 'darwin' && app.dock) {
261
+ try {
262
+ app.dock.setIcon(ICON);
263
+ } catch (err) {
264
+ console.error('could not set dock icon', err);
265
+ }
266
+ }
267
+
268
+ buildMenu();
269
+ restoreNotes();
270
+
271
+ app.on('activate', () => {
272
+ if (windows.size === 0) restoreNotes();
273
+ else windows.forEach((w) => w.show());
274
+ });
275
+ });
276
+
277
+ // Stickies live on even with no window open, matching the macOS app.
278
+ app.on('window-all-closed', () => {});
279
+ app.on('before-quit', () => store.saveNow());
280
+
281
+ ipcMain.handle('note:get', (_e, id) => store.get(id));
282
+ ipcMain.handle('note:update', (_e, patch) => store.upsert(patch));
283
+
284
+ // A closing note flushes its last edit here. Write straight through rather than
285
+ // joining the debounce: the window is already going away.
286
+ ipcMain.on('note:flush', (e, patch) => {
287
+ if (patch && patch.id) {
288
+ store.upsert(patch);
289
+ store.saveNow();
290
+ }
291
+ e.returnValue = true;
292
+ });
293
+ ipcMain.handle('note:new', () => createNote().id);
294
+
295
+ ipcMain.handle('note:delete', (_e, id) => {
296
+ store.remove(id);
297
+ const win = windows.get(id);
298
+ if (win && !win.isDestroyed()) win.destroy();
299
+ });
300
+
301
+ ipcMain.handle('note:setAlwaysOnTop', (e, value) => {
302
+ const win = BrowserWindow.fromWebContents(e.sender);
303
+ if (win) win.setAlwaysOnTop(!!value);
304
+ });
305
+
306
+ ipcMain.handle('note:close', (e) => BrowserWindow.fromWebContents(e.sender)?.close());
307
+
308
+ ipcMain.handle('note:openExternal', (_e, url) => openExternal(url));
package/src/preload.js ADDED
@@ -0,0 +1,27 @@
1
+ const { contextBridge, ipcRenderer, clipboard } = require('electron');
2
+
3
+ const arg = process.argv.find((a) => a.startsWith('--note-id='));
4
+ const noteId = arg ? arg.slice('--note-id='.length) : null;
5
+
6
+ contextBridge.exposeInMainWorld('sticky', {
7
+ noteId,
8
+ get: () => ipcRenderer.invoke('note:get', noteId),
9
+ update: (patch) => ipcRenderer.invoke('note:update', { id: noteId, ...patch }),
10
+ create: () => ipcRenderer.invoke('note:new'),
11
+ remove: () => ipcRenderer.invoke('note:delete', noteId),
12
+ setAlwaysOnTop: (v) => ipcRenderer.invoke('note:setAlwaysOnTop', v),
13
+ close: () => ipcRenderer.invoke('note:close'),
14
+ openExternal: (url) => ipcRenderer.invoke('note:openExternal', url),
15
+ // Electron's clipboard rather than navigator.clipboard, which is
16
+ // unreliable for a page served over file://.
17
+ copy: (text) => clipboard.writeText(String(text)),
18
+ // Synchronous on purpose. This is called while the window is being torn
19
+ // down, where an async invoke() would race the renderer's destruction and
20
+ // lose the edit. sendSync blocks until main has the text.
21
+ flush: (body) => ipcRenderer.sendSync('note:flush', { id: noteId, body }),
22
+ on: (channel, fn) => {
23
+ const allowed = ['toggle-edit', 'request-delete', 'font-size', 'always-on-top-changed'];
24
+ if (!allowed.includes(channel)) return;
25
+ ipcRenderer.on(channel, (_e, payload) => fn(payload));
26
+ },
27
+ });