latex-stickies 1.4.2 → 1.4.3

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/README.md CHANGED
@@ -169,6 +169,7 @@ node scripts/render-check.js # the first-run note renders correctly
169
169
  node scripts/ghost-check.js # autocomplete suggests, and Tab accepts
170
170
  node scripts/snapshot-check.js # a long note is captured whole
171
171
  node scripts/conflict-check.js # the changed-on-disk banner behaves
172
+ node scripts/denied-check.js # a notes folder it may not read is reported
172
173
  node scripts/verify-install.js # the install path, end to end (macOS)
173
174
  ```
174
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latex-stickies",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Sticky notes for your desktop that render LaTeX and Markdown.",
5
5
  "main": "src/main.js",
6
6
  "scripts": {
@@ -25,6 +25,9 @@
25
25
  ],
26
26
  "mac": {
27
27
  "category": "public.app-category.productivity",
28
+ "extendInfo": {
29
+ "NSDocumentsFolderUsageDescription": "LaTeX Stickies keeps your notes as Markdown files in Documents, so you can read and edit them with any other app."
30
+ },
28
31
  "target": [
29
32
  "dmg",
30
33
  "zip"
@@ -94,6 +94,10 @@ function isCurrent(version) {
94
94
  }
95
95
  }
96
96
 
97
+ /** Shown in the macOS prompt asking to let the app read the notes folder. */
98
+ const DOCUMENTS_REASON = 'LaTeX Stickies keeps your notes as Markdown files in '
99
+ + 'Documents, so you can read and edit them with any other app.';
100
+
97
101
  function setPlist(plist, key, value) {
98
102
  try {
99
103
  run('plutil', ['-replace', key, '-string', value, plist]);
@@ -126,6 +130,10 @@ function build(app, version) {
126
130
  setPlist(plist, 'CFBundleDisplayName', NAME);
127
131
  setPlist(plist, 'CFBundleIdentifier', APP_ID);
128
132
  setPlist(plist, 'CFBundleExecutable', NAME);
133
+ // Without this key macOS denies the notes folder without ever asking. The
134
+ // app then starts, reads nothing and shows no window, which looks exactly
135
+ // like a launch that failed.
136
+ setPlist(plist, 'NSDocumentsFolderUsageDescription', DOCUMENTS_REASON);
129
137
 
130
138
  const from = path.join(TARGET, 'Contents', 'MacOS', 'Electron');
131
139
  const to = path.join(TARGET, 'Contents', 'MacOS', NAME);
@@ -20,7 +20,9 @@ import {
20
20
  defaultKeymap, history, historyKeymap, indentWithTab,
21
21
  cursorLineUp, cursorLineDown, selectLineUp, selectLineDown,
22
22
  } from '@codemirror/commands';
23
- import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
23
+ import {
24
+ markdown, markdownLanguage, insertNewlineContinueMarkup,
25
+ } from '@codemirror/lang-markdown';
24
26
  import {
25
27
  syntaxTree, HighlightStyle, syntaxHighlighting, defaultHighlightStyle,
26
28
  LanguageDescription,
@@ -70,7 +72,7 @@ window.CM = {
70
72
  EditorView, keymap, Decoration, WidgetType, ViewPlugin,
71
73
  defaultKeymap, history, historyKeymap, indentWithTab,
72
74
  cursorLineUp, cursorLineDown, selectLineUp, selectLineDown,
73
- markdown, markdownLanguage,
75
+ markdown, markdownLanguage, insertNewlineContinueMarkup,
74
76
  syntaxTree, HighlightStyle, syntaxHighlighting, defaultHighlightStyle,
75
77
  tags, codeLanguages,
76
78
  search, searchKeymap, highlightSelectionMatches, openSearchPanel,
package/src/main.js CHANGED
@@ -527,6 +527,37 @@ function watchNotesFolder() {
527
527
  });
528
528
  }
529
529
 
530
+ /**
531
+ * macOS can refuse the notes folder outright, and quietly.
532
+ *
533
+ * Documents is protected, and permission is tied to the app's signature -- so
534
+ * a rebuilt copy is a different app to the system and starts again with no
535
+ * access. Reading the folder then fails, nothing is restored and no window
536
+ * opens, which is indistinguishable from a launch that crashed. Say what
537
+ * happened instead, and offer the settings pane that fixes it.
538
+ */
539
+ function reportNoAccess(err) {
540
+ // Printed as well as shown: the dialog is the answer for the person at the
541
+ // screen, this line is what a harness or a bug report can see.
542
+ console.error(`notes folder refused: ${err.code} ${store.DIR}`);
543
+ const settings = 'x-apple.systempreferences:com.apple.preference.security'
544
+ + '?Privacy_DocumentsFolder';
545
+ const choice = dialog.showMessageBoxSync({
546
+ type: 'error',
547
+ message: 'LaTeX Stickies cannot read your notes',
548
+ detail: `macOS is not letting it open ${store.DIR}.\n\n`
549
+ + 'Allow it under Privacy & Security, in Files and Folders, then open '
550
+ + `the app again.\n\n(${err.code}: ${err.message})`,
551
+ buttons: ['Open Privacy Settings', 'Quit'],
552
+ defaultId: 0,
553
+ });
554
+ if (choice === 0 && process.platform === 'darwin') shell.openExternal(settings);
555
+ app.quit();
556
+ }
557
+
558
+ /** True when the filesystem said no, rather than the folder being absent. */
559
+ const denied = (err) => err && (err.code === 'EPERM' || err.code === 'EACCES');
560
+
530
561
  app.whenReady().then(async () => {
531
562
  // Run from npm there is no .app bundle of our own to carry the icon, so
532
563
  // macOS would show the generic Electron atom in the Dock. Set it explicitly.
@@ -546,9 +577,15 @@ app.whenReady().then(async () => {
546
577
  }
547
578
  }
548
579
 
549
- await buildMenu();
550
- restoreNotes();
551
- watchNotesFolder();
580
+ try {
581
+ await buildMenu();
582
+ restoreNotes();
583
+ watchNotesFolder();
584
+ } catch (err) {
585
+ if (!denied(err)) throw err;
586
+ reportNoAccess(err);
587
+ return;
588
+ }
552
589
 
553
590
  if (SMOKE_CLOSE_MS) {
554
591
  setTimeout(() => {
@@ -19,7 +19,7 @@ const {
19
19
  WidgetType, ViewPlugin, keymap, Prec,
20
20
  defaultKeymap, history, historyKeymap, indentWithTab,
21
21
  cursorLineUp, cursorLineDown, selectLineUp, selectLineDown,
22
- markdown, markdownLanguage, codeLanguages,
22
+ markdown, markdownLanguage, codeLanguages, insertNewlineContinueMarkup,
23
23
  syntaxTree, HighlightStyle, syntaxHighlighting, defaultHighlightStyle, tags,
24
24
  search, searchKeymap, highlightSelectionMatches,
25
25
  } = window.CM;
@@ -312,6 +312,8 @@ const HIDE = Decoration.replace({});
312
312
  */
313
313
  const MARKER = Decoration.mark({ class: 'cm-md-marker' });
314
314
 
315
+
316
+
315
317
  /** Elements whose markers are shown while the caret is inside them. */
316
318
  const REVEAL = new Set([
317
319
  'Emphasis', 'StrongEmphasis', 'Strikethrough', 'InlineCode', 'FencedCode',
@@ -616,8 +618,13 @@ const markerReveal = ViewPlugin.fromClass(class {
616
618
  this.sync();
617
619
  }
618
620
 
619
- update(update) {
620
- if (update.docChanged || update.selectionSet || update.viewportChanged) this.sync();
621
+ update() {
622
+ // Every update, not only the ones that moved the caret. The parser runs
623
+ // behind the keystroke, so the first "#" of a heading arrives as a plain
624
+ // character and becomes a marker on a later update that changed neither
625
+ // the document nor the selection. Skipping those left the reveal one
626
+ // keystroke behind: a heading stayed hidden while it was being typed.
627
+ this.sync();
621
628
  }
622
629
 
623
630
  destroy() {
@@ -630,12 +637,21 @@ const markerReveal = ViewPlugin.fromClass(class {
630
637
  let from = -1;
631
638
  let to = -1;
632
639
  if (!snapshotMode) {
633
- for (let node = syntaxTree(view.state).resolveInner(sel.from, 1); node; node = node.parent) {
634
- if (REVEAL.has(node.name) && sel.to <= node.to) {
635
- from = node.from;
636
- to = node.to;
637
- break;
640
+ const tree = syntaxTree(view.state);
641
+ // Both sides of the caret, and the side before it first. Typing "###"
642
+ // leaves the caret at the end of the line, where the node *starting*
643
+ // here is the document, not the heading -- so looking forward only, a
644
+ // heading stayed hidden the whole time you were typing it.
645
+ const enclosing = (side) => {
646
+ for (let node = tree.resolveInner(sel.from, side); node; node = node.parent) {
647
+ if (REVEAL.has(node.name) && sel.to <= node.to) return node;
638
648
  }
649
+ return null;
650
+ };
651
+ const node = enclosing(-1) || enclosing(1);
652
+ if (node) {
653
+ from = node.from;
654
+ to = node.to;
639
655
  }
640
656
  }
641
657
  for (const el of view.contentDOM.querySelectorAll('.cm-md-marker')) {
@@ -669,9 +685,24 @@ const livePreview = StateField.define({
669
685
 
670
686
  /* ---------- how markdown reads ---------- */
671
687
 
688
+ /**
689
+ * Markers are punctuation, not prose, and should not compete with the text
690
+ * they mark up. CodeMirror's default paints them #404740 -- all but the ink
691
+ * colour -- because every marker carries tags.processingInstruction, a child
692
+ * of meta, and defaultHighlightStyle is loaded here for its code colours.
693
+ *
694
+ * A flat grey rather than a faded ink: fading a marker into the paper is what
695
+ * made an H1 and an H2 indistinguishable while one was being typed.
696
+ */
697
+ const MARKER_INK = '#757575';
698
+
672
699
  const markdownStyle = HighlightStyle.define([
673
700
  // textDecoration: 'none' is deliberate -- defaultHighlightStyle, loaded for
674
701
  // the colours inside code fences, underlines every heading.
702
+ // One entry covers every marker: the hashes, the stars, the backticks, the
703
+ // quote arrow, the list dash, a table's pipes and a strikethrough's tildes
704
+ // all carry this tag.
705
+ { tag: tags.processingInstruction, color: MARKER_INK },
675
706
  { tag: tags.heading, textDecoration: 'none' },
676
707
  { tag: tags.heading1, fontSize: '1.35em', fontWeight: '600', textDecoration: 'none' },
677
708
  { tag: tags.heading2, fontSize: '1.18em', fontWeight: '600', textDecoration: 'none' },
@@ -777,6 +808,17 @@ function verticalStep(forward, base, extend) {
777
808
  };
778
809
  }
779
810
 
811
+ /**
812
+ * Enter carries a list on: a new bullet, the next number, another empty task
813
+ * box, or a second quote line -- and a second Enter on an item with nothing in
814
+ * it ends the list instead of adding to it.
815
+ *
816
+ * Without this, pressing Enter after "- [ ] milk" left a bare line, and typing
817
+ * "[ ] eggs" there looks like a task and is not one: the box only means
818
+ * anything inside a list item.
819
+ */
820
+ const continueList = [{ key: 'Enter', run: insertNewlineContinueMarkup }];
821
+
780
822
  const verticalKeymap = [
781
823
  { key: 'ArrowUp', run: verticalStep(false, cursorLineUp), preventDefault: true },
782
824
  { key: 'ArrowDown', run: verticalStep(true, cursorLineDown), preventDefault: true },
@@ -804,6 +846,7 @@ function createLiveEditor({ parent, doc, onChange }) {
804
846
  // Ahead of the defaults, so Mod-i and friends are not swallowed.
805
847
  Prec.high(keymap.of(shortcuts)),
806
848
  Prec.high(keymap.of(verticalKeymap)),
849
+ Prec.high(keymap.of(continueList)),
807
850
  // Search ahead of the defaults: Cmd+F must open the panel rather than
808
851
  // fall through to anything else bound to it.
809
852
  Prec.high(keymap.of(searchKeymap)),
@@ -237,9 +237,13 @@ body.editing #tools > * { visibility: visible; }
237
237
  opacity: 0;
238
238
  transition: max-width 0.12s ease, opacity 0.12s ease;
239
239
  }
240
+ /* Full strength, not a hint. A revealed marker is there to be edited -- you
241
+ are counting hashes to see what level a heading is, or putting the caret
242
+ between a pair of stars -- and a washed-out one is hard to read against
243
+ the paper. It is the hiding that keeps the note clean, not the fading. */
240
244
  .cm-editor .cm-md-marker-open {
241
245
  max-width: 4em;
242
- opacity: 0.35;
246
+ opacity: 1;
243
247
  }
244
248
 
245
249
  /* Sits at the end of a block's opening fence line, beside the language name. */