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 @@
⚙️
+
+