diff --git a/src/util/cooklang_to_latex.rs b/src/util/cooklang_to_latex.rs index 2e98506..d576e40 100644 --- a/src/util/cooklang_to_latex.rs +++ b/src/util/cooklang_to_latex.rs @@ -239,9 +239,9 @@ fn write_ingredients(w: &mut impl io::Write, recipe: &Recipe, converter: &Conver )?; } - if ingredient.reference.is_some() { + if let Some(reference) = &ingredient.reference { let sep = std::path::MAIN_SEPARATOR.to_string(); - let path = ingredient.reference.as_ref().unwrap().components.join(&sep); + let path = reference.components.join(&sep); write!( w, r"\ingredient{{{}}}", diff --git a/src/util/cooklang_to_md.rs b/src/util/cooklang_to_md.rs index 4abfdd4..b4a5f4a 100644 --- a/src/util/cooklang_to_md.rs +++ b/src/util/cooklang_to_md.rs @@ -332,9 +332,9 @@ fn ingredients( } } - if ingredient.reference.is_some() { + if let Some(reference) = &ingredient.reference { let sep = std::path::MAIN_SEPARATOR.to_string(); - let path = ingredient.reference.as_ref().unwrap().components.join(&sep); + let path = reference.components.join(&sep); write!( w, "[{}]({}{}{})", diff --git a/src/util/cooklang_to_typst.rs b/src/util/cooklang_to_typst.rs index f4b5271..e05da06 100644 --- a/src/util/cooklang_to_typst.rs +++ b/src/util/cooklang_to_typst.rs @@ -206,9 +206,9 @@ fn write_ingredients(w: &mut impl io::Write, recipe: &Recipe, converter: &Conver write!(w, r"*{}* ", escape_typst(&entry.quantity.to_string()))?; } - if ingredient.reference.is_some() { + if let Some(reference) = &ingredient.reference { let sep = std::path::MAIN_SEPARATOR.to_string(); - let path = ingredient.reference.as_ref().unwrap().components.join(&sep); + let path = reference.components.join(&sep); write!( w, r#"#ingredient("{}")"#, diff --git a/static/js/keyboard-shortcuts.js b/static/js/keyboard-shortcuts.js new file mode 100644 index 0000000..06dac4d --- /dev/null +++ b/static/js/keyboard-shortcuts.js @@ -0,0 +1,394 @@ +/** + * Keyboard shortcuts for Cook CLI web interface + * + * Global shortcuts (available on all pages): + * - / Focus search + * - g h Go to home/recipes + * - g s Go to shopping list + * - g p Go to pantry + * - g x Go to preferences + * - ? Show keyboard shortcuts help + * - Escape Close modals/dropdowns + * - t Toggle theme (dark/light) + * + * Recipe page shortcuts: + * - e Edit recipe + * - a Add to shopping list + * - p Print recipe + * - +/- Increase/decrease scale + * - [/] Decrease/increase scale by 0.5 + */ + +(function() { + 'use strict'; + + // Track pending key sequences (for multi-key shortcuts like "g h") + let pendingKey = null; + let pendingTimeout = null; + + // Check if user is typing in an input field + function isTyping(event) { + const target = event.target; + const tagName = target.tagName.toLowerCase(); + + // Check for input elements + if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') { + return true; + } + + // Check for contenteditable elements + if (target.isContentEditable) { + return true; + } + + // Check for CodeMirror editor + if (target.closest('.cm-editor')) { + return true; + } + + return false; + } + + // Clear pending key sequence + function clearPendingKey() { + pendingKey = null; + if (pendingTimeout) { + clearTimeout(pendingTimeout); + pendingTimeout = null; + } + } + + // Show keyboard shortcuts modal + window.showShortcutsHelp = function() { + const existingModal = document.getElementById('keyboard-shortcuts-modal'); + if (existingModal) { + existingModal.classList.remove('hidden'); + return; + } + + const modal = document.createElement('div'); + modal.id = 'keyboard-shortcuts-modal'; + modal.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50'; + modal.innerHTML = ` +
+
+

Keyboard Shortcuts

+ +
+
+
+
+

Navigation

+
+
+ Focus search + / +
+
+ Go to recipes + g h +
+
+ Go to shopping list + g s +
+
+ Go to pantry + g p +
+
+ Go to preferences + g x +
+
+
+
+

General

+
+
+ Toggle theme + t +
+
+ Show shortcuts + ? +
+
+ Close modal + Esc +
+
+
+
+

Recipe Page

+
+
+ Edit recipe + e +
+
+ Add to shopping list + a +
+
+ Print recipe + p +
+
+ Increase scale + + +
+
+ Decrease scale + - +
+
+
+
+

Shopping List

+
+
+ Clear all items + c +
+
+
+
+
+
+ Press Esc to close +
+
+ `; + + document.body.appendChild(modal); + + // Close on backdrop click + modal.addEventListener('click', function(e) { + if (e.target === modal) { + closeShortcutsHelp(); + } + }); + } + + // Close keyboard shortcuts modal + window.closeShortcutsHelp = function() { + const modal = document.getElementById('keyboard-shortcuts-modal'); + if (modal) { + modal.classList.add('hidden'); + } + }; + + // Handle keyboard events + function handleKeydown(event) { + // Don't handle if user is typing + if (isTyping(event)) { + // But still allow Escape to blur inputs + if (event.key === 'Escape') { + event.target.blur(); + } + return; + } + + // Don't handle if modifier keys are pressed (except Shift for ? and +) + if (event.ctrlKey || event.metaKey || event.altKey) { + return; + } + + const key = event.key; + + // Handle pending key sequences (like "g h") + if (pendingKey === 'g') { + clearPendingKey(); + switch (key) { + case 'h': + case 'r': + event.preventDefault(); + window.location.href = '/'; + return; + case 's': + event.preventDefault(); + window.location.href = '/shopping-list'; + return; + case 'p': + event.preventDefault(); + window.location.href = '/pantry'; + return; + case 'x': + event.preventDefault(); + window.location.href = '/preferences'; + return; + } + // If no valid second key, fall through to handle as new key + } + + // Global shortcuts + switch (key) { + case '/': + event.preventDefault(); + const searchInput = document.getElementById('search-input'); + if (searchInput) { + searchInput.focus(); + searchInput.select(); + } + return; + + case 'g': + event.preventDefault(); + pendingKey = 'g'; + // Clear pending after 1.5 seconds + pendingTimeout = setTimeout(clearPendingKey, 1500); + return; + + case '?': + event.preventDefault(); + showShortcutsHelp(); + return; + + case 'Escape': + event.preventDefault(); + // Close shortcuts modal if open + const shortcutsModal = document.getElementById('keyboard-shortcuts-modal'); + if (shortcutsModal && !shortcutsModal.classList.contains('hidden')) { + closeShortcutsHelp(); + return; + } + // Close search results if open + const searchResults = document.getElementById('search-results'); + if (searchResults && !searchResults.classList.contains('hidden')) { + searchResults.classList.add('hidden'); + return; + } + // Close any other modals + const modals = document.querySelectorAll('[data-modal]'); + modals.forEach(modal => modal.classList.add('hidden')); + return; + + case 't': + event.preventDefault(); + if (typeof toggleTheme === 'function') { + toggleTheme(); + } + return; + } + + // Page-specific shortcuts + const path = window.location.pathname; + + // Recipe page shortcuts + if (path.startsWith('/recipe/')) { + handleRecipeShortcuts(event, key); + } + // Shopping list page shortcuts + else if (path === '/shopping-list') { + handleShoppingListShortcuts(event, key); + } + } + + // Recipe page specific shortcuts + function handleRecipeShortcuts(event, key) { + switch (key) { + case 'e': + event.preventDefault(); + // Find and click the edit link + const editLink = document.querySelector('a[href^="/edit/"]'); + if (editLink) { + editLink.click(); + } + return; + + case 'a': + event.preventDefault(); + // Find and click the add to shopping list button + const addButton = document.querySelector('button[onclick^="addToShoppingList"]'); + if (addButton) { + addButton.click(); + } + return; + + case 'p': + event.preventDefault(); + window.print(); + return; + + case '+': + case '=': // = is on the same key as + without shift + event.preventDefault(); + adjustScale(0.5); + return; + + case '-': + case '_': + event.preventDefault(); + adjustScale(-0.5); + return; + + case ']': + event.preventDefault(); + adjustScale(1); + return; + + case '[': + event.preventDefault(); + adjustScale(-1); + return; + } + } + + // Adjust recipe scale + function adjustScale(delta) { + const scaleInput = document.getElementById('scale'); + if (!scaleInput) return; + + let newValue = parseFloat(scaleInput.value) + delta; + const min = parseFloat(scaleInput.min) || 0.5; + const max = parseFloat(scaleInput.max) || 200; + + // Clamp to valid range + newValue = Math.max(min, Math.min(max, newValue)); + + // Round to avoid floating point issues + newValue = Math.round(newValue * 10) / 10; + + if (newValue !== parseFloat(scaleInput.value)) { + scaleInput.value = newValue; + // Trigger the onchange event + scaleInput.dispatchEvent(new Event('change')); + } + } + + // Shopping list page specific shortcuts + function handleShoppingListShortcuts(event, key) { + switch (key) { + case 'c': + event.preventDefault(); + // Clear the list (if the function exists) + if (typeof clearList === 'function') { + if (confirm('Clear all items from shopping list?')) { + clearList(); + } + } + return; + } + } + + // Initialize keyboard shortcuts + document.addEventListener('keydown', handleKeydown); + + // Add visual indicator for keyboard navigation + document.addEventListener('DOMContentLoaded', function() { + // Add a small hint in the search placeholder about the shortcut + const searchInput = document.getElementById('search-input'); + if (searchInput) { + const currentPlaceholder = searchInput.getAttribute('placeholder'); + if (currentPlaceholder && !currentPlaceholder.includes('/')) { + // Don't modify placeholder - keep it clean + } + } + }); + +})(); diff --git a/templates/base.html b/templates/base.html index 47e94c2..e5034c2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -498,6 +498,12 @@ ⚙️ + +