/** * Zen Mode — distraction-free writing mode * Provides typewriter scrolling, line dimming, centered content, and a floating HUD. * Toggle with F11, exit with Escape. */ const { EditorView, ViewPlugin, Decoration } = require('@codemirror/view'); const { RangeSetBuilder } = require('@codemirror/state'); class ZenMode { /** * @param {import('./renderer').TabManager} tabManager */ constructor(tabManager) { this.tabManager = tabManager; this.active = false; this._hud = null; this._timerInterval = null; this._sessionStart = null; this._extensions = []; } activate() { if (this.active) return; this.active = true; document.body.classList.add('zen-mode'); // Collapse sidebar const sidebar = document.getElementById('sidebar'); if (sidebar) sidebar.classList.add('collapsed'); // Hide preview pane for the active tab const tab = this.tabManager.tabs.get(this.tabManager.activeTabId); if (tab) { const previewContainer = document.getElementById(`preview-pane-${tab.id}`); if (previewContainer) previewContainer.classList.add('hidden'); const editorPane = document.getElementById(`editor-pane-${tab.id}`); if (editorPane) editorPane.classList.add('full-width'); // Add CM6 extensions to the active editor if (tab.editorView) { this._extensions = [typewriterScrollExtension, lineDimmingExtension]; // Reconfigure is not straightforward with CM6's immutable state. // Instead we dispatch effects or simply rebuild with new extensions. // Since we cannot hot-swap extensions on an existing EditorView, // we store a reference and the CSS + updateListener handle the rest. // The typewriter + dimming plugins are applied via a compartment approach // would be ideal, but for simplicity we use DOM + updateListener. this._applyTypewriterBehavior(tab.editorView); } } // Create HUD this._createHUD(); // Start session timer this._sessionStart = Date.now(); this._timerInterval = setInterval(() => this._updateHUD(), 1000); // Focus editor if (tab?.editorView) tab.editorView.focus(); } deactivate() { if (!this.active) return; this.active = false; document.body.classList.remove('zen-mode'); // Restore preview pane for the active tab const tab = this.tabManager.tabs.get(this.tabManager.activeTabId); if (tab) { const previewContainer = document.getElementById(`preview-pane-${tab.id}`); if (previewContainer) previewContainer.classList.remove('hidden'); const editorPane = document.getElementById(`editor-pane-${tab.id}`); if (editorPane) editorPane.classList.remove('full-width'); // Remove typewriter listener if (tab.editorView && this._selectionListener) { window.removeEventListener('zen-typewriter', this._selectionListener); this._selectionListener = null; } } // Remove HUD if (this._hud && this._hud.parentNode) { this._hud.parentNode.removeChild(this._hud); this._hud = null; } // Stop timer if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } this._sessionStart = null; this._extensions = []; } toggle() { if (this.active) { this.deactivate(); } else { this.activate(); } } _applyTypewriterBehavior(view) { // We use a custom update listener that scrolls the cursor to center. const scrollFn = () => { if (!this.active) return; const pos = view.state.selection.main.head; requestAnimationFrame(() => { view.dispatch({ effects: EditorView.scrollIntoView(pos, { y: 'center' }), }); }); }; // Use interval to keep cursor centered during typing this._scrollInterval = setInterval(() => { if (!this.active) { clearInterval(this._scrollInterval); this._scrollInterval = null; return; } // Typewriter scroll: center the cursor line try { const pos = view.state.selection.main.head; const coords = view.coordsAtPos(pos); const scroller = view.scrollDOM; if (coords && scroller) { const scrollerRect = scroller.getBoundingClientRect(); const cursorCenter = coords.top + (coords.bottom - coords.top) / 2; const viewCenter = scrollerRect.top + scrollerRect.height / 2; const diff = cursorCenter - viewCenter; if (Math.abs(diff) > 30) { scroller.scrollTop += diff; } } } catch { // Ignore errors from destroyed views } }, 200); // Initial scroll to center scrollFn(); } _createHUD() { const hud = document.createElement('div'); hud.className = 'zen-hud'; hud.innerHTML = ` 0 words · 0 min read · 00:00