/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace) Quick file opener and command runner. Inspired by VS Code's command palette. */ import { api } from './auth.js'; import { openFile } from './viewer.js'; import { showToast, toggleTheme, TabManager } from './ui.js'; import { openEditor } from './utils.js'; import { state } from './state.js'; // ── Helper: trigger goHome by clicking the logo ── function triggerGoHome() { const logo = document.getElementById('header-logo'); if (logo) logo.click(); } // ── Get current active file from tabs ── function getCurrentFile() { const activeId = TabManager._activeTabId; if (activeId) { const tab = TabManager._tabs.find(t => t.id === activeId); if (tab) return { vault: tab.vault, path: tab.path }; } // Fallback to state if (state.currentVault && state.currentPath) { return { vault: state.currentVault, path: state.currentPath }; } return null; } // ── Get current vault from tabs ── function getCurrentVault() { const file = getCurrentFile(); if (file) return file.vault; if (state.currentVault) return state.currentVault; if (state.allVaults && state.allVaults.length > 0) return state.allVaults[0].name; return null; } // ── Prompt for vault if ambiguous ── function promptVault(hint) { const vault = getCurrentVault(); if (vault) return vault; return prompt(`${hint} — Nom de la vault :`) || ''; } // ── DOM cache ── let _overlay = null; let _input = null; let _results = null; let _mode = 'files'; let _selectedIndex = 0; let _searchTimeout = null; let _lastResults = []; // ── Available commands ── const COMMANDS = [ // ── Navigation ── { id: 'home', label: '🏠 Accueil', description: 'Retour à l\'accueil', category: 'Navigation', action: () => { close(); triggerGoHome(); } }, // ── Fichiers ── { id: 'edit-current', label: '✏️ Éditer fichier courant', description: 'Ouvrir le fichier actif dans l\'éditeur', category: 'Fichiers', action: () => { close(); const f = getCurrentFile(); if (f) openEditor(f.vault, f.path); else showToast('Aucun fichier ouvert', 'error'); } }, { id: 'popout', label: '🪟 Pop-out', description: 'Ouvrir le document courant dans une nouvelle fenêtre', category: 'Fichiers', action: () => { close(); const f = getCurrentFile(); if (f) window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}`, `popout_${f.vault}_${f.path.replace(/[^a-zA-Z0-9]/g, '_')}`, 'width=1000,height=700,menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=no'); else showToast('Aucun fichier ouvert', 'error'); } }, { id: 'create-file', label: '📄 Créer fichier markdown', description: 'Créer un nouveau fichier .md et l\'ouvrir dans l\'éditeur', category: 'Fichiers', action: async () => { close(); await createFile(); } }, { id: 'delete-current', label: '🗑️ Supprimer fichier courant', description: 'Supprimer le fichier ouvert dans l\'onglet actif', category: 'Fichiers', action: async () => { close(); await deleteCurrentFile(); } }, // ── Répertoires ── { id: 'create-dir', label: '📁 Créer répertoire', description: 'Créer un nouveau dossier', category: 'Répertoires', action: async () => { close(); await createDirectory(); } }, { id: 'delete-dir', label: '🗑️ Supprimer répertoire', description: 'Supprimer un dossier et son contenu', category: 'Répertoires', action: async () => { close(); await deleteDirectory(); } }, // ── Actions ── { id: 'theme', label: '🌓 Changer le thème', description: 'Basculer entre thème clair et sombre', category: 'Actions', action: () => { close(); toggleTheme(); } }, { id: 'reindex', label: '🔄 Réindexer', description: 'Forcer la réindexation complète des vaults', category: 'Actions', action: async () => { close(); try { await api('/api/index/reload'); showToast('Index rechargé', 'success'); } catch { showToast('Erreur de réindexation', 'error'); } } }, // ── Aide & Config ── { id: 'help', label: '❓ Aide', description: 'Ouvrir le guide d\'utilisation', category: 'Système', action: () => { close(); const btn = document.getElementById('help-open-btn'); if (btn) btn.click(); } }, { id: 'config', label: '⚙️ Configuration', description: 'Ouvrir les paramètres', category: 'Système', action: () => { close(); const btn = document.getElementById('config-open-btn'); if (btn) btn.click(); } }, ]; // ── Create file action ── async function createFile() { const vault = promptVault('Créer un fichier'); if (!vault) return; let path = prompt('Chemin du fichier (ex: notes/mon-fichier) :'); if (!path) return; // Add .md extension if none if (!path.includes('.')) path += '.md'; try { const data = await api(`/api/file/${encodeURIComponent(vault)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path, content: `# ${path.split('/').pop().replace('.md', '')}\n\n` }), }); if (data?.status === 'ok' || data?.success) { showToast(`✅ ${path} créé`, 'success'); openEditor(vault, path); } else { showToast('Erreur de création', 'error'); } } catch (e) { showToast(`Erreur : ${e.message || e}`, 'error'); } } // ── Create directory action ── async function createDirectory() { const vault = promptVault('Créer un dossier'); if (!vault) return; const path = prompt('Chemin du dossier (ex: notes/projets) :'); if (!path) return; try { const data = await api(`/api/directory/${encodeURIComponent(vault)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path }), }); if (data?.status === 'ok' || data?.success) { showToast(`📁 ${path} créé`, 'success'); } else { showToast('Erreur de création', 'error'); } } catch (e) { showToast(`Erreur : ${e.message || e}`, 'error'); } } // ── Delete current file action ── async function deleteCurrentFile() { const f = getCurrentFile(); if (!f) { showToast('Aucun fichier ouvert', 'error'); return; } if (!confirm(`Supprimer définitivement "${f.path}" ?`)) return; try { const data = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { method: 'DELETE', }); if (data?.status === 'ok' || data?.success) { showToast(`🗑️ ${f.path} supprimé`, 'success'); // Close the tab const tab = TabManager._tabs.find(t => t.vault === f.vault && t.path === f.path); if (tab) TabManager.close(tab.id); triggerGoHome(); } else { showToast('Erreur de suppression', 'error'); } } catch (e) { showToast(`Erreur : ${e.message || e}`, 'error'); } } // ── Delete directory action ── async function deleteDirectory() { const vault = promptVault('Supprimer un dossier'); if (!vault) return; const path = prompt('Chemin du dossier à supprimer (ex: notes/obsolète) :'); if (!path) return; if (!confirm(`Supprimer définitivement le dossier "${path}" et TOUT son contenu ?`)) return; try { const data = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method: 'DELETE', }); if (data?.status === 'ok' || data?.success) { showToast(`🗑️ ${path} supprimé`, 'success'); } else { showToast('Erreur de suppression', 'error'); } } catch (e) { showToast(`Erreur : ${e.message || e}`, 'error'); } } // ── Create DOM structure ── function createDOM() { if (_overlay) return; _overlay = document.createElement('div'); _overlay.className = 'command-palette-overlay'; _overlay.innerHTML = `
Ctrl+Shift+Espace
`; _input = _overlay.querySelector('.cp-input'); _results = _overlay.querySelector('.cp-results'); _overlay.addEventListener('click', (e) => { if (e.target === _overlay) close(); }); _input.addEventListener('keydown', onKeyDown); _input.addEventListener('input', onInput); document.body.appendChild(_overlay); } export function open(initialQuery = '') { createDOM(); _overlay.classList.add('active'); _input.value = initialQuery; _mode = initialQuery.startsWith('>') ? 'commands' : 'files'; _selectedIndex = 0; _lastResults = []; _results.innerHTML = ''; _input.focus(); onInput(); } export function close() { if (!_overlay) return; _overlay.classList.remove('active'); if (_searchTimeout) clearTimeout(_searchTimeout); _searchTimeout = null; const searchInput = document.querySelector('.search-input'); if (searchInput) searchInput.focus(); } function isOpen() { return _overlay && _overlay.classList.contains('active'); } function onInput() { const val = _input.value; const newMode = val.startsWith('>') ? 'commands' : 'files'; if (newMode !== _mode) { _mode = newMode; _input.placeholder = _mode === 'commands' ? 'Tapez une commande...' : 'Rechercher un fichier... (tapez > pour les commandes)'; } if (_searchTimeout) clearTimeout(_searchTimeout); _searchTimeout = setTimeout(() => { _searchTimeout = null; if (_mode === 'commands') { searchCommands(val.slice(1).trim()); } else { searchFiles(val.trim()); } }, 150); } async function searchFiles(query) { if (!query) { _results.innerHTML = '
Commencez à taper pour chercher un fichier
'; _lastResults = []; return; } try { const data = await api(`/api/suggest?q=${encodeURIComponent(query)}&vault=all`); const suggestions = data.suggestions || []; _lastResults = suggestions.map(s => ({ type: 'file', vault: s.vault, path: s.path, title: s.title, label: s.title || s.path.split('/').pop(), sublabel: `${s.vault}/${s.path}`, })); renderResults(); } catch { _results.innerHTML = '
Erreur de recherche
'; } } function searchCommands(query) { if (!query) { _lastResults = COMMANDS.map(c => ({ type: 'command', ...c })); renderResults(); return; } const lower = query.toLowerCase(); _lastResults = COMMANDS .filter(c => c.label.toLowerCase().includes(lower) || c.description.toLowerCase().includes(lower) || (c.category && c.category.toLowerCase().includes(lower))) .map(c => ({ type: 'command', ...c })); renderResults(); } function renderResults() { _selectedIndex = 0; if (_lastResults.length === 0) { _results.innerHTML = '
Aucun résultat
'; return; } _results.innerHTML = _lastResults.map((item, i) => `
${escapeHtml(item.label)}
${escapeHtml(item.sublabel || item.description || '')}
`).join(''); _results.querySelectorAll('.cp-item').forEach(el => { el.addEventListener('click', () => { const idx = parseInt(el.dataset.index); executeItem(idx); }); el.addEventListener('mouseenter', () => { document.querySelectorAll('.cp-item').forEach(e => e.classList.remove('cp-selected')); el.classList.add('cp-selected'); _selectedIndex = parseInt(el.dataset.index); }); }); scrollToSelected(); } function onKeyDown(e) { const items = _results.querySelectorAll('.cp-item'); if (e.key === 'Escape') { e.preventDefault(); close(); } else if (e.key === 'Enter') { e.preventDefault(); executeItem(_selectedIndex); } else if (e.key === 'ArrowDown') { e.preventDefault(); _selectedIndex = Math.min(_selectedIndex + 1, _lastResults.length - 1); updateSelection(items); } else if (e.key === 'ArrowUp') { e.preventDefault(); _selectedIndex = Math.max(_selectedIndex - 1, 0); updateSelection(items); } else if (e.key === 'Tab') { e.preventDefault(); if (_mode === 'files') { _input.value = '> '; setTimeout(() => { _input.selectionStart = _input.selectionEnd = _input.value.length; }, 0); } else { _input.value = ''; } onInput(); } } function updateSelection(items) { items.forEach((el, i) => { el.classList.toggle('cp-selected', i === _selectedIndex); }); scrollToSelected(); } function scrollToSelected() { const selected = _results.querySelector('.cp-selected'); if (selected) { selected.scrollIntoView({ block: 'nearest' }); } } function executeItem(index) { const item = _lastResults[index]; if (!item) return; if (item.type === 'file') { close(); openFile(item.vault, item.path); } else if (item.type === 'command') { item.action(); } } function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } export function initCommandPalette() { document.addEventListener('keydown', (e) => { // Ctrl+Shift+Espace — file palette if ((e.ctrlKey || e.metaKey) && e.shiftKey && !e.altKey && (e.key === ' ' || e.code === 'Space')) { e.preventDefault(); if (isOpen()) { close(); } else { open(''); } } // Ctrl+Alt+Espace — command palette if ((e.ctrlKey || e.metaKey) && e.altKey && !e.shiftKey && (e.key === ' ' || e.code === 'Space')) { e.preventDefault(); if (isOpen()) { close(); } else { open('> '); } } }); }