/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace) */ import { api } from './auth.js'; import { showToast, toggleTheme, TabManager } from './ui.js'; import { openEditor } from './utils.js'; import { state } from './state.js'; import { t } from './i18n.js'; function triggerGoHome() { const e = document.getElementById('header-logo'); if (e) e.click(); } function getCurrentFile() { const id = TabManager._activeTabId; if (id) { const t = TabManager._tabs.find(x => x.id === id); if (t) return { vault: t.vault, path: t.path }; } if (state.currentVault && state.currentPath) return { vault: state.currentVault, path: state.currentPath }; return null; } // ── Query parser: +word = required, -word = exclude, word = required ── function parseQuery(raw) { const positive = []; const negative = []; const parts = (raw || '').split(/\s+/).filter(Boolean); for (const p of parts) { if (p.startsWith('-') && p.length > 1) negative.push(p.slice(1).toLowerCase()); else if (p.startsWith('+')) positive.push(p.slice(1).toLowerCase()); else positive.push(p.toLowerCase()); } return { positive, negative }; } // ── DOM / state ── let _overlay = null, _input = null, _results = null, _mode = 'files', _sel = 0, _to = null, _items = []; let _browse = { action: null, vault: null, path: '' }; let _browseFilterEl = null, _browseFilter = '', _allItems = []; let _browseFilterTimer = null; // debounce for global search // ── Commands ── (lazy: built on first access so i18n is ready) let _cmdsCache = null; function getCMDS() { if (_cmdsCache) return _cmdsCache; _cmdsCache = [ { id:'home', label:t('palette.cmd_back_home'), desc:t('palette.cmd_back_home_desc'), cat:'Navigation', act:()=>{close();triggerGoHome();} }, { id:'edit-current', label:'✏️ '+t('editor.edit'), desc:t('editor.edit_current_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)openEditor(f.vault,f.path);else showToast(t('toast.no_open_file'),'error');} }, { id:'popout', label:'🪟 Pop-out', desc:t('palette.popout_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}?theme=${document.documentElement.getAttribute('data-theme')||'dark'}`,'popout','width=1000,height=700');else showToast(t('toast.no_open_file'),'error');} }, { id:'create-file', label:t('palette.cmd_create_file'), desc:t('palette.cmd_create_file_desc'), cat:'Fichiers', act:()=>startBrowse('create-file') }, { id:'delete-file', label:'🗑️ '+t('file.delete'), desc:t('palette.delete_file_desc'), cat:'Fichiers', act:()=>startBrowse('delete-file') }, { id:'delete-cur', label:'🗑️ '+t('palette.delete_current'), desc:t('palette.delete_current_desc'), cat:'Fichiers', act:async()=>{close();await delCurFile();} }, { id:'close-cur', label:'✕ '+t('palette.close_current'), desc:t('palette.close_current_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f){const tab=TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path);if(tab)TabManager.close(tab.id);else triggerGoHome();}else{showToast(t('toast.no_open_file'),'error');}} }, { id:'create-dir', label:t('palette.cmd_create_dir'), desc:t('palette.cmd_create_dir_desc'), cat:'Répertoires', act:()=>startBrowse('create-dir') }, { id:'delete-dir', label:t('palette.cmd_delete_dir'), desc:t('palette.cmd_delete_dir_desc'), cat:'Répertoires', act:()=>startBrowse('delete-dir') }, { id:'theme', label:t('palette.cmd_theme'), desc:t('palette.cmd_theme_desc'), cat:'Actions', act:()=>{close();toggleTheme();} }, { id:'reindex', label:'🔄 '+t('vault.reindex'), desc:t('palette.reindex_desc'), cat:'Actions', act:async()=>{close();try{await api('/api/index/reload');showToast(t('sync.index_reloaded'),'success');}catch{showToast(t('common.error'),'error');}} }, { id:'help', label:'❓ '+t('help.help_title'), desc:t('palette.help_desc'), cat:'Système', act:()=>{close();const b=document.getElementById('help-open-btn');if(b)b.click();} }, { id:'config', label:t('palette.cmd_config'), desc:t('palette.cmd_config_desc'), cat:'Système', act:()=>{close();const b=document.getElementById('config-open-btn');if(b)b.click();} }, // Pane / Split commands { id:'split-right', label:'⊞→ Diviser à droite', desc:'Crée un panneau à droite avec cet onglet', cat:'Panneaux', act:()=>{close();if(window.PaneManager)window.PaneManager.splitRight();} }, { id:'split-down', label:'⊞↓ Diviser en bas', desc:'Crée un panneau en dessous avec cet onglet', cat:'Panneaux', act:()=>{close();if(window.PaneManager)window.PaneManager.splitDown();} }, { id:'close-pane', label:'✕ Fermer le panneau actif', desc:'Ferme le panneau actif et tous ses onglets', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit())window.PaneManager.closeActivePane();} }, { id:'focus-next', label:'→ Panneau suivant', desc:'Active le panneau suivant (Ctrl+Alt+→)', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit()){const n=(window.PaneManager.activePaneId+1)%window.PaneManager.panes.length;window.PaneManager.setActivePane(n);}} }, { id:'focus-prev', label:'← Panneau précédent', desc:'Active le panneau précédent (Ctrl+Alt+←)', cat:'Panneaux', act:()=>{close();if(window.PaneManager&&window.PaneManager.isSplit()){const n=(window.PaneManager.activePaneId-1+window.PaneManager.panes.length)%window.PaneManager.panes.length;window.PaneManager.setActivePane(n);}} }, { id:'reset-panes', label:'🔄 Réinitialiser les panneaux', desc:'Ferme tous les panneaux et revient au mode single-pane', cat:'Panneaux', act:()=>{close();if(window.PaneManager){while(window.PaneManager.isSplit()){window.PaneManager.closePane(window.PaneManager.panes.length-1);}localStorage.removeItem('obsigate-panes');}} }, // BooksLM commands { id:'bookslm-open', label:t('palette.bookslm_open'), desc:'Open BooksLM AI chat for current directory', cat:'AI', act:async()=>{close();const f=getCurrentFile();if(f){const m=await import('./bookslm.js');m.default.open(f.vault,f.path);}else{showToast(t('toast.no_open_file'),'error');}} }, { id:'bookslm-new', label:t('palette.bookslm_new'), desc:'Reset BooksLM conversation and open fresh', cat:'AI', act:async()=>{close();const f=getCurrentFile();if(f){const m=await import('./bookslm.js');m.default.newConversation();m.default.open(f.vault,f.path);}else{showToast(t('toast.no_open_file'),'error');}} }, ]; return _cmdsCache; } // ── Browse: vault picker ── function startBrowse(action) { _browse = { action, vault: null, path: '' }; _browseFilter = ''; _allItems = []; switchMode('browse-vault'); renderVaultPicker(); } async function renderVaultPicker() { try { const data = await api('/api/vaults'); const vaults = Array.isArray(data) ? data : []; _items = vaults.map(v => ({ type:'_vault', vault: v.name, label: (v.type==='DIR'?'📂':'🗂️')+' '+v.name, sublabel: t('palette.vault_count', {count: v.file_count}) })); if (_items.length === 0) { _items = [{ type:'_empty', label: t('palette.no_vaults'), sublabel: '' }]; } _allItems = _items; render(); } catch { _items = [{ type:'_error', label: t('palette.error_loading'), sublabel: '' }]; _allItems = _items; render(); } } // ── Browse: directory navigation ── async function renderBrowse() { const { vault, path } = _browse; _items = []; const bc = document.getElementById('cp-breadcrumb'); if (bc) { bc.style.display = 'flex'; const segs = path ? path.split('/') : []; let html = `${esc(vault)}`; let acc = ''; for (const s of segs) { acc = acc ? acc+'/'+s : s; html += `/${esc(s)}`; } bc.innerHTML = html; bc.querySelectorAll('.cp-bc-item[data-path]').forEach(el => el.addEventListener('click', ()=>{ _browse.path=el.dataset.path; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); })); bc.querySelector('.cp-bc-root').addEventListener('click', ()=>{ _browse.path=''; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); }); } try { const data = await api(`/api/browse/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`); const items = data.items || []; const dirs = items.filter(i => i.type === 'directory' || i.is_dir); const files = items.filter(i => i.type === 'file' && !i.is_dir); // Action row const actLabel = { 'create-file': t('palette.cmd_create_file'), 'create-dir': t('palette.cmd_create_dir'), 'delete-dir': t('palette.cmd_delete_dir'), 'delete-file': t('palette.delete_file_desc'), 'open-file': t('palette.browse_open_file') }; const isDeleteDir = _browse.action === 'delete-dir' && path; const isDeleteFile = _browse.action === 'delete-file'; const isOpenFile = _browse.action === 'open-file'; const needsName = _browse.action === 'create-file' || _browse.action === 'create-dir'; if (isDeleteDir) { _items.push({ type:'_action', label: '🗑️ '+t('file.delete')+' '+(path.split('/').pop()||path), sublabel: t('palette.dir_delete_confirm'), act:()=>execDelDir(path) }); } else if (needsName) { _items.push({ type:'_action', label: actLabel[_browse.action]||'', sublabel: t('palette.browse_in', {path: path||t('palette.browse_root')}), act:()=>askName() }); } // Parent if (path) { _items.push({ type:'_nav', label: '📂 .. (parent)', go: path.split('/').slice(0,-1).join('/') }); } // Subdirs for (const d of dirs) { const fp = path ? path+'/'+d.name : d.name; _items.push({ type:'_nav', label: '📁 '+d.name, go: fp }); } // Files for (const f of files) { const fp = path ? path+'/'+f.name : f.name; if (isDeleteFile) { _items.push({ type:'_file', label: '📄 '+f.name, sublabel: f.vault+'/'+fp, act:()=>execDelFile(fp) }); } else if (isOpenFile) { _items.push({ type:'_file', label: '📄 '+f.name, sublabel: f.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(vault, fp); } }); } else { _items.push({ type:'_info', label: '📄 '+f.name, sublabel: fp }); } } if (_items.length === 0) _items.push({ type:'_empty', label: t('palette.empty_dir'), sublabel: '' }); } catch { _items = [{ type:'_error', label: t('common.error'), sublabel: '' }]; } _allItems = _items; applyBrowseFilter(); } // ── Browse filter: local when empty, global suggest when text ── function applyBrowseFilter() { const q = _browseFilter.trim(); if (!q) { // Empty filter: show local directory items _items = _allItems; render(); return; } // Has filter text: debounce and search globally if (_browseFilterTimer) clearTimeout(_browseFilterTimer); _browseFilterTimer = setTimeout(() => searchBrowseGlobal(q), 200); } async function searchBrowseGlobal(rawQ) { const { positive, negative } = parseQuery(rawQ); const action = _browse.action; const isDeleteFile = action === 'delete-file'; const isOpenFile = action === 'open-file'; const isDeleteDir = action === 'delete-dir'; const needsName = action === 'create-file' || action === 'create-dir'; try { const q = positive.join(' '); const vault = _browse.vault || 'all'; const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=${encodeURIComponent(vault)}`); const sug = (d.suggestions || []).filter(s => { const fullPath = (s.vault+'/'+s.path).toLowerCase(); // Client-side negative filter for (const n of negative) { if (fullPath.includes(n)) return false; } return true; }); if (sug.length === 0) { _items = [{ type:'_empty', label: t('palette.no_results', {query: rawQ}), sublabel: '' }]; render(); return; } _items = []; // Extract unique directories from result paths const dirSet = new Set(); for (const s of sug) { const parts = s.path.split('/'); // Build parent directories for (let i = 0; i < parts.length - 1; i++) { dirSet.add(s.vault + '|' + parts.slice(0, i + 1).join('/')); } } // Show directories as navigable const dirs = [...dirSet].map(d => { const [v, p] = d.split('|'); const name = p.split('/').pop(); return { type:'_nav', label:'📁 '+name, sublabel:v+'/'+p, go:p, vault:v }; }).sort((a,b) => a.sublabel.localeCompare(b.sublabel)); for (const d of dirs) _items.push(d); // Show files for (const s of sug) { const fp = s.path; const name = fp.split('/').pop(); if (isDeleteFile) { _items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>execDelFileBrowsed(s.vault, fp) }); } else if (isOpenFile) { _items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(s.vault, fp); } }); } else if (isDeleteDir) { _items.push({ type:'_file', label:'📁 '+name, sublabel:s.vault+'/'+fp, act:()=>execDelDirBrowsed(s.vault, fp) }); } else { _items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(s.vault, fp); } }); } } render(); } catch { _items = [{ type:'_error', label: t('palette.search_error'), sublabel: '' }]; render(); } } // Navigate to a directory from global search function execNavGlobal(vault, goPath) { _browse.vault = vault; _browse.path = goPath; _browseFilter = ''; if (_browseFilterEl) _browseFilterEl.value = ''; renderBrowse(); } async function execDelFileBrowsed(vault, path) { if (!confirm(t("confirm.delete_file_named", {path: vault + "/" + path}))) return; try { const d = await api(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' }); if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); } else showToast(t('common.error'),'error'); } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } async function execDelDirBrowsed(vault, path) { if (!confirm(t("confirm.delete_dir_named", {path: path}))) return; try { const d = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' }); if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); } else showToast(t('common.error'),'error'); } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } function onBrowseFilterInput() { _browseFilter = _browseFilterEl ? _browseFilterEl.value : ''; applyBrowseFilter(); } function onBrowseFilterKey(e) { if (e.key === 'Escape') { e.preventDefault(); if (_browseFilter) { _browseFilter = ''; if (_browseFilterEl) _browseFilterEl.value = ''; applyBrowseFilter(); } else { close(); } } else if (e.key === 'ArrowDown') { e.preventDefault(); _sel = 0; updSel(_results.querySelectorAll('.cp-item')); _overlay.focus(); } else if (e.key === 'ArrowUp') { e.preventDefault(); const items = _results.querySelectorAll('.cp-item'); _sel = Math.max(items.length - 1, 0); updSel(items); _overlay.focus(); } else if (e.key === 'Enter') { e.preventDefault(); if (_items.length > 0) exec(_sel); } else if (e.key === 'Backspace' && !_browseFilter && (!_browseFilterEl || !_browseFilterEl.value)) { e.preventDefault(); const segs = _browse.path ? _browse.path.split('/') : []; if (segs.length > 0) { _browse.path = segs.slice(0, -1).join('/'); renderBrowse(); } else if (_browse.vault) { switchMode('browse-vault'); renderVaultPicker(); } } } // ── Name input modal ── function askName() { const label = _browse.action === 'create-file' ? 'Nom du fichier' : 'Nom du dossier'; const ext = _browse.action === 'create-file' ? '(.md ajouté si besoin)' : ''; const existingInput = document.getElementById('cp-name-input'); if (existingInput) { existingInput.focus(); return; } _input.style.display = 'none'; const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none'; const nameDiv = document.createElement('div'); nameDiv.id = 'cp-name-input'; nameDiv.className = 'cp-name-input'; nameDiv.innerHTML = ` Enter confirmer · Esc annuler`; _results.before(nameDiv); const field = document.getElementById('cp-name-field'); field.focus(); field.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); const name = field.value.trim(); if (!name) return; cleanupNameInput(); if (_browse.action === 'create-file') execCreateFile(name); else execCreateDir(name); } else if (e.key === 'Escape') { e.preventDefault(); cleanupNameInput(); _overlay.focus(); renderBrowse(); } }); } function cleanupNameInput() { const el = document.getElementById('cp-name-input'); if (el) el.remove(); const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'flex'; } // ── Execute actions ── async function execCreateFile(name) { let fp = _browse.path ? _browse.path+'/'+name : name; if (!fp.includes('.')) fp += '.md'; try { const d = await api(`/api/file/${encodeURIComponent(_browse.vault)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path:fp, content:`# ${name.replace('.md','')}\n\n`}) }); if (d?.status==='ok' || d?.success) { showToast(`✅ ${fp} créé`,'success'); close(); openEditor(_browse.vault, fp); } else showToast(t('toast.create_error'),'error'); } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } async function execCreateDir(name) { const dp = _browse.path ? _browse.path+'/'+name : name; try { const d = await api(`/api/directory/${encodeURIComponent(_browse.vault)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path:dp}) }); if (d?.status==='ok' || d?.success) { showToast(`📁 ${dp} créé`,'success'); close(); refreshSidebar(); } else showToast(t('common.error'),'error'); } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } async function execDelDir(path) { if (!confirm(t("confirm.delete_dir_named", {path: path}))) return; try { const d = await api(`/api/directory/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' }); if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); } else showToast(t('common.error'),'error'); } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } async function execDelFile(path) { if (!confirm(t("confirm.delete_file_named", {path: path}))) return; try { const d = await api(`/api/file/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' }); if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); } else showToast(t('common.error'),'error'); } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } async function delCurFile() { const f = getCurrentFile(); if (!f) { showToast(t('toast.no_open_file'),'error'); return; } if (!confirm(t("confirm.delete_file_named", {path: f.path}))) return; try { const d = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { method:'DELETE' }); if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${f.path} supprimé`,'success'); const tab = TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path); if(tab) TabManager.close(tab.id); triggerGoHome(); refreshSidebar(); } else showToast(t('common.error'),'error'); } catch(e) { showToast('Erreur : '+(e.message||e),'error'); } } function refreshSidebar() { import('./sidebar.js').then(m => { if (m.loadVaults) m.loadVaults(); }).catch(() => {}); } // ── DOM ── function createDOM() { if (_overlay) return; _overlay = document.createElement('div'); _overlay.className = 'command-palette-overlay'; _overlay.tabIndex = -1; _overlay.innerHTML = `