/**
* 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·
`;
document.body.appendChild(hud);
this._hud = hud;
this._wireGoalButton();
this._updateHUD();
}
/**
* The word-goal progress bar reads localStorage('zen-word-goal'), but until
* now nothing in the app ever set it. The HUD button opens a small inline
* input (window.prompt is unavailable in Electron) that sets or clears it.
*/
_wireGoalButton() {
const btn = this._hud?.querySelector('#zen-goal-btn');
if (!btn) return;
btn.addEventListener('click', () => {
const current = localStorage.getItem('zen-word-goal') || '';
const overlay = document.createElement('div');
overlay.className = 'zen-goal-overlay';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.innerHTML = `