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.
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/latex-stickies.js +36 -0
- package/build/icon.icns +0 -0
- package/build/icon.png +0 -0
- package/package.json +76 -0
- package/src/main.js +308 -0
- package/src/preload.js +27 -0
- package/src/renderer/editor.js +201 -0
- package/src/renderer/markdown.js +154 -0
- package/src/renderer/note.css +288 -0
- package/src/renderer/note.html +34 -0
- package/src/renderer/note.js +210 -0
- package/src/renderer/vendor/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-Bold.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-Italic.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Math-Italic.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Script-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/src/renderer/vendor/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/src/renderer/vendor/katex.min.css +1 -0
- package/src/renderer/vendor/katex.min.js +1 -0
- package/src/renderer/vendor/marked.js +80 -0
- package/src/renderer/vendor/prism.js +2009 -0
- package/src/renderer/vendor/purify.js +3 -0
- 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.');
|
package/build/icon.icns
ADDED
|
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
|
+
});
|