// Command palette (Ctrl/Cmd-K) and quick capture. Uses the app's globals (allowed, TABS, icon, jfetch, openSheet, store, toast ...). /** "buy milk tomorrow" -> { title: 'buy milk', due: '2026-10-23' }. Understands today, tomorrow, a weekday name, "in N days" and YYYY-MM-DD at the end. */ function parseQuickTask(text) { let t = String(text || '').trim(), due = ''; const d0 = new Date(); d0.setHours(12, 0, 0, 0); const fmt = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; let m; if ((m = /\s+(\d{4}-\d{2}-\d{2})$/.exec(t))) { due = m[1]; t = t.slice(0, m.index); } else if ((m = /\s+in (\d{1,3}) days?$/i.exec(t))) { const d = new Date(d0); d.setDate(d.getDate() + +m[1]); due = fmt(d); t = t.slice(0, m.index); } else if ((m = /\s+(today|tomorrow|tonight)$/i.exec(t))) { const d = new Date(d0); if (/tomorrow/i.test(m[1])) d.setDate(d.getDate() + 1); due = fmt(d); t = t.slice(0, m.index); } else if ((m = /\s+(?:on |next )?(sun|mon|tue|wed|thu|fri|sat)[a-z]*$/i.exec(t))) { const d = new Date(d0), want = days.indexOf(m[1].toLowerCase()); let add = (want - d.getDay() + 7) % 7; if (add === 0) add = 7; d.setDate(d.getDate() + add); due = fmt(d); t = t.slice(0, m.index); } return { title: t.trim() || String(text || '').trim(), due }; } async function quickTask(text) { const { title, due } = parseQuickTask(text); if (!title) return; await jfetch('/api/tasks', { method: 'POST', body: JSON.stringify({ list: 'local:inbox', fields: { title, notes: '', due, done: false } }) }); toast('Task added' + (due ? ' (due ' + due + ')' : '')); if (location.hash.startsWith('#/tasks') && typeof viewTasks === 'function') viewTasks(); } /** Small sheet to capture something fast from any screen. */ function quickAdd() { const can = t => allowed(t); const el = openSheet('Quick add', '', `${can('tasks') ? `

Understands today, tomorrow, a weekday, "in 3 days" or a date (2026-12-25) at the end.

` : ''}
${can('calendar') ? '' : ''}${can('links') ? '' : ''}
`); const qt = el.querySelector('#qt'); if (qt) qt.focus(); if (el.querySelector('#qf')) el.querySelector('#qf').onsubmit = async e => { e.preventDefault(); try { await quickTask(qt.value); closeSheet(); } catch (err) { el.querySelector('#qerr').textContent = err.message; } }; if (el.querySelector('#qe')) el.querySelector('#qe').onclick = () => { closeSheet(); newEvent(ymd(new Date())); }; if (el.querySelector('#ql')) el.querySelector('#ql').onclick = () => { closeSheet(); linkSheet(); }; } // ---- Command palette let palEl = null; function openPalette() { if (palEl) return closePalette(); const lastFocus = document.activeElement; palEl = document.createElement('div'); palEl.id = 'pal'; palEl.innerHTML = `
`; document.body.appendChild(palEl); const input = palEl.querySelector('#palq'), list = palEl.querySelector('#pall'); let items = [], sel = 0, seq = 0, timer, live = []; const go = h => () => { location.hash = h; }; const base = () => { const out = navTabs().flatMap(t => MERGED[t[0]] ? MERGED[t[0]][1].filter(allowed).map(id => [id, tabLabel(id)]) : [t]).map(([id, label]) => ({ label: 'Go to ' + label, hint: (navGroups().find(g => g.tabs.some(t => t[0] === (leafOf(id) || id))) || {}).label || 'Page', ic: TAB_ICON[id], run: go('#/' + id), kw: label })); if (phone.matches) out.push({ label: 'Edit the bottom bar', hint: 'View', ic: 'menu', run: () => openPinsSheet(), kw: 'menu tabs pins more' }); if (allowed('tasks') && allowed('calendar')) out.push({ label: 'Go to Tasks', hint: 'Planner', ic: 'check', run: go('#/tasks'), kw: 'todo' }, { label: 'Go to Calendar', hint: 'Planner', ic: 'cal', run: go('#/calendar'), kw: 'agenda events' }); if (allowed('tasks')) out.push({ label: 'New task', hint: 'Create', ic: 'plus', run: () => quickAdd(), kw: 'add todo' }); if (allowed('calendar')) out.push({ label: 'New event', hint: 'Create', ic: 'plus', run: () => newEvent(ymd(new Date())), kw: 'add calendar' }); if (allowed('mail') && cfg.mail) out.push({ label: 'New email', hint: 'Create', ic: 'mail', run: () => { location.hash = '#/mail'; setTimeout(() => mailCompose(), 300); }, kw: 'compose write message' }); if (allowed('office')) for (const [k, o] of Object.entries(OFFICE_KINDS)) out.push({ label: 'New ' + o.one.toLowerCase(), hint: 'Create', ic: 'doc', run: () => { location.hash = '#/office'; setTimeout(() => newOfficeDoc(o.exts[0], o.one.toLowerCase()), 250); }, kw: 'office word excel powerpoint ' + o.exts.join(' ') }); if (allowed('links')) out.push({ label: 'New link', hint: 'Create', ic: 'plus', run: () => linkSheet(), kw: 'add bookmark' }); out.push({ label: 'Hide or show the menu', hint: 'View', ic: 'menu', run: () => setNav({ hidden: !navPref().hidden }), kw: 'sidebar' }); out.push({ label: 'Refresh the app', hint: 'App', ic: 'refresh', run: () => location.reload(), kw: 'reload update' }); return out; }; const build = () => { const q = input.value.trim(), ql = q.toLowerCase(); let all = base(); if (typeof allLinks === 'function' && allowed('links')) all = all.concat(allLinks().map(l => ({ label: l.title || l.url, hint: 'Link · ' + new URL(l.url).host, ic: 'link', run: () => window.open(l.url, '_blank', 'noopener'), kw: (l.group || '') + ' ' + l.url }))); let res = ql ? all.filter(i => (i.label + ' ' + i.kw).toLowerCase().includes(ql)) : all.slice(0, 12); if (ql) { res = res.sort((a, b) => (b.label.toLowerCase().startsWith(ql)) - (a.label.toLowerCase().startsWith(ql))); res = res.concat(live); if (allowed('tasks')) res.push({ label: 'Add task: ' + q, hint: 'Create', ic: 'plus', run: () => quickTask(q).catch(e => toast(e.message)) }); if (allowed('search')) res.push({ label: 'Search everything for “' + q + '”', hint: 'Search', ic: 'search', run: () => { window.pendingSearch = q; if (location.hash === '#/search') route(); else location.hash = '#/search'; } }); } items = res; sel = Math.min(sel, Math.max(0, items.length - 1)); list.innerHTML = items.length ? items.map((i, n) => `
${icon(i.ic)}${esc(i.label)}${esc(i.hint)}
`).join('') : '

Nothing matches.

'; input.setAttribute('aria-activedescendant', items.length ? 'po' + sel : ''); const on = list.querySelector('.on'); if (on) on.scrollIntoView({ block: 'nearest' }); }; const fetchLive = q => { // tasks and events from the server, shown under the commands clearTimeout(timer); live = []; if (q.length < 2 || !(allowed('tasks') || allowed('calendar'))) return; const mine = ++seq; timer = setTimeout(async () => { try { const r = await jfetch('/api/search?q=' + encodeURIComponent(q)); if (mine !== seq || !palEl) return; live = (r.tasks || []).slice(0, 4).map(t => ({ label: t.title, hint: 'Task' + (t.due ? ' · due ' + t.due : '') + (t.done ? ' · done' : ''), ic: 'check', run: go('#/tasks') })) .concat((r.events || []).slice(0, 4).map(e => ({ label: e.title, hint: 'Event · ' + (e.allDay ? e.start : new Date(e.start).toLocaleDateString([], { month: 'short', day: 'numeric' })), ic: 'cal', run: go('#/calendar') }))); build(); } catch {} }, 250); }; const run = n => { const it = items[n]; if (!it) return; closePalette(); setTimeout(it.run, 0); }; input.oninput = () => { sel = 0; fetchLive(input.value.trim()); build(); }; input.onkeydown = e => { if (e.key === 'ArrowDown') { e.preventDefault(); sel = (sel + 1) % Math.max(1, items.length); build(); } else if (e.key === 'ArrowUp') { e.preventDefault(); sel = (sel - 1 + items.length) % Math.max(1, items.length); build(); } else if (e.key === 'Enter') { e.preventDefault(); run(sel); } else if (e.key === 'Escape') { e.preventDefault(); closePalette(); } else if (e.key === 'Tab') e.preventDefault(); // keep focus in the box }; list.onclick = e => { const r = e.target.closest('.palrow'); if (r) run(+r.dataset.n); }; palEl.querySelector('.scrim').onclick = closePalette; palEl._restore = lastFocus; build(); input.focus(); requestAnimationFrame(() => palEl && palEl.classList.add('on')); } function closePalette() { if (!palEl) return; const r = palEl._restore; palEl.remove(); palEl = null; try { r && r.focus && r.focus(); } catch {} } addEventListener('keydown', e => { if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'k') { if (document.body.classList.contains('editing') || !document.getElementById('nav')) return; // not on the sign-in page or inside the document editor e.preventDefault(); openPalette(); } });