Action
shortcuts launcher
This is an update on the music launcher I posted, adding:
∙ Conditional display: Items can be shown or hidden based on conditions like time of day, calendar events, or draft queries
∙ Shortcuts integration: Use shortcuts:Shortcut Name syntax to trigger iOS Shortcuts e.g. [link title](shortcuts:Shortcut Name)— no need for manual URL encoding!
∙ Drafts actions: Use drafts:Action Name syntax to run Drafts actions directly e.g. [link title](drafts:Action Name)
∙ Edit button: Click the ✎ button (top right) to open the source draft for editing
∙ Slight tweaks for legibility
You can use this version in the same way you might have used the earlier URL launcher, but it does have a bit more code for conditional checks (drafts queries and calendars).
The action renders an interface based on a list of URLs in a source draft, organised by Markdown headings. The first heading in the list is unfolded; entries under other headings are folded by default. Headings with no links underneath them aren’t rendered. The interface parses #tags and offers the ability to filter the visible list by those tags, with a search bar for filtering by item titles.
I’m using this version to make menus on the fly, particularly a menu for some of my most used actions that don’t fit in my action bar, some of which only need to be available under certain conditions.
## Start-up
+ [Morning Breathwork](shortcuts:Morning Breathwork) CONDITIONAL: noEventWithText("@(breathwork)")
+ [Initialise daily note](drafts:Today's journal) CONDITIONAL: noDailyNote()
## Main Menu
+ [Today's Journal](drafts:Today's journal)
+ [Music](drafts:music launcher)
+ [Shopping List](drafts://open?uuid=A8FFD648-A510-4CC2-90C8-297D624AA340)
+ [Log bodywork](drafts:log bodywork)
Configuration:
Update this line and point it to the source draft for your launcher:
const LAUNCHER_DRAFT_TITLE = "::launcher";
To check specific calendars for conditional events (leave empty array to check all):
const CALENDARS_TO_CHECK = ["Work", "Personal"];
Font families can be customised if you don’t have Hoefler or Roboto Mono installed.
Conditional syntax:
Add CONDITIONAL: functionName() at the end of any line to control visibility:
e.g.
+ [Morning Breathwork](shortcuts:Breathwork) CONDITIONAL: noEventWithText("@(breathwork)")
+ [Initialise daily note](drafts:Daily Note) CONDITIONAL: noDraftTodayMatching("daily note")
+ [Morning Routine](shortcuts:Morning) CONDITIONAL: isTimeRange(5, 12)
+ [Weekly Review](shortcuts:Review) CONDITIONAL: isDayOfWeek(0)
Available conditional functions:
Available conditional functions:
- noDraftTodayMatching("tag") or noDraftTodayMatching("tag1", "tag2", "titlePattern") - Show only if no draft matching the specified criteria was created today. Can search by one or more tags, and optionally filter by title substring. The function searches the “all” folder (inbox + archive). Examples:
``noDraftTodayMatching("daily note")`` - Show if no draft tagged "daily note" was created today
``noDraftTodayMatching("daily note", "» JOURNAL")`` - Show if no draft tagged "daily note" with "» JOURNAL" in the title was created today
``noDraftTodayMatching("journal", "review", "weekly")`` - Show if no draft with both "journal" AND "review" tags containing "weekly" in the title was created today
noEventWithText("text")- Show only if no calendar event today contains the texthasEventWithText("text")- Show only if a calendar event today contains the textisTimeRange(startHour, endHour)- Show only during specified hours (24-hour format)isDayOfWeek(dayNum)- Show only on specific day (0=Sunday, 6=Saturday)isWeekday()- Show only Monday-FridayisWeekend()- Show only Saturday-SundayhasTaggedDraft("tag")- Show only if drafts with specified tag exist
Interface:
Full screen interface. Click the × in the top-right corner to cancel.
Steps
-
script
// ======================================== // URL LAUNCHER SCRIPT with CONDITIONAL ITEMS // ======================================== // ---------- CONFIG ---------- const LAUNCHER_DRAFT_TITLE = "::shortcuts launcher"; const TITLE_FONT = "'Hoefler Text','Baskerville','Garamond','Times New Roman',serif"; const MONO_FONT = "'Roboto Mono','SF Mono',ui-monospace,Menlo,Consolas,monospace"; // Specify calendars to check for events (empty array = check all calendars) // Examples: ["Calendar", "Work", "Personal"] const CALENDARS_TO_CHECK = []; // Enable debug logging const DEBUG = false; // ---------- LOGGER ---------- function log(s) { if (DEBUG) console.log(s); } // ---------- ONE-RUN CACHES ---------- const _cache = { draftQueryByTag: {}, // key: "scope|tag" -> drafts[] calendarList: null, // calendars[] eventsByCalKey: {}, // key: cal.identifier||cal.title -> events[] actionURLByName: {} // key: raw actionName -> resolved URL }; function todayRange() { const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const end = new Date(start.getTime() + 86400000); return { start, end }; } function queryDraftsByTag(scope, tag) { const key = scope + "|" + tag; if (_cache.draftQueryByTag[key]) return _cache.draftQueryByTag[key]; const results = Draft.query("", scope, [tag]) || []; _cache.draftQueryByTag[key] = results; return results; } // ---------- CALENDAR HELPERS (CACHED) ---------- function getCalendarsToCheck() { if (_cache.calendarList) return _cache.calendarList; if (CALENDARS_TO_CHECK.length === 0) { _cache.calendarList = Calendar.getAllCalendars(); } else { _cache.calendarList = CALENDARS_TO_CHECK .map(function(name) { return Calendar.findOrCreate(name); }) .filter(function(cal) { return cal !== null; }); } return _cache.calendarList; } function getEventsForCalendarToday(cal) { const key = cal.identifier || cal.title || String(cal); if (_cache.eventsByCalKey[key]) return _cache.eventsByCalKey[key]; const r = todayRange(); const events = cal.events(r.start, r.end) || []; _cache.eventsByCalKey[key] = events; return events; } // ---------- CONDITIONAL FUNCTIONS ---------- const CONDITIONS = { // Check if no draft exists matching tag(s) and/or title pattern created today // Usage: noDraftTodayMatching("tag1") or noDraftTodayMatching("tag1", "tag2") // or noDraftTodayMatching("tag1", "tag2", "titlePattern") noDraftTodayMatching: function() { const r = todayRange(); const args = Array.prototype.slice.call(arguments); if (args.length === 0) return true; // No criteria = show item // Last argument might be a title pattern if it doesn't look like a tag let titlePattern = null; let tags = args; // If last arg contains spaces or special chars, treat it as title pattern if (args.length > 0 && (args[args.length - 1].includes(" ") || args[args.length - 1].includes("»"))) { titlePattern = args[args.length - 1]; tags = args.slice(0, -1); } log("\n=== noDraftTodayMatching() Debug ==="); log("Tags: " + (tags.length ? tags.join(", ") : "none")); log("Title pattern: " + (titlePattern || "none")); // Query by tags if provided, otherwise query all let results; if (tags.length > 0) { results = queryDraftsByTag("all", tags[0]); // Filter by additional tags if provided for (let i = 1; i < tags.length; i++) { results = results.filter(function(d) { return d.tags.includes(tags[i]); }); } } else { results = Draft.query("", "all", []) || []; } log("Found " + results.length + " drafts matching tags"); // Filter by title pattern if provided if (titlePattern) { results = results.filter(function(d) { return d.title.includes(titlePattern); }); log("After title filter: " + results.length + " drafts"); } // Check if any draft was created today const hasToday = results.some(function(d) { const createdDate = new Date(d.createdAt); const isToday = createdDate >= r.start && createdDate < r.end; if (DEBUG && isToday) { log(' Draft: "' + d.title + '"'); log(" Created: " + createdDate); } return isToday; }); log("Has today's matching draft: " + hasToday); log("Should show item: " + (!hasToday)); log("======================\n"); return !hasToday; }, // Check if no calendar event today contains specific text in title noEventWithText: function(searchText) { const calendars = getCalendarsToCheck(); for (let cal of calendars) { const events = getEventsForCalendarToday(cal); const hasEvent = events.some(function(e) { return (e.title || "").includes(searchText); }); if (hasEvent) return false; } return true; }, // Check if any calendar event today contains specific text in title hasEventWithText: function(searchText) { const calendars = getCalendarsToCheck(); for (let cal of calendars) { const events = getEventsForCalendarToday(cal); const hasEvent = events.some(function(e) { return (e.title || "").includes(searchText); }); if (hasEvent) return true; } return false; }, // Check time-based conditions isTimeRange: function(startHour, endHour) { const now = new Date(); const hour = now.getHours(); return hour >= startHour && hour < endHour; }, // Check if specific tag exists in drafts hasTaggedDraft: function(tag) { const results = queryDraftsByTag("inbox", tag); return results.length > 0; }, // Day of week check (0 = Sunday, 6 = Saturday) isDayOfWeek: function(dayNum) { return new Date().getDay() === dayNum; }, // Check if it's a weekday (Monday-Friday) isWeekday: function() { const day = new Date().getDay(); return day >= 1 && day <= 5; }, // Check if it's a weekend (Saturday-Sunday) isWeekend: function() { const day = new Date().getDay(); return day === 0 || day === 6; } }; // ---------- PARSE CONDITIONAL SYNTAX ---------- function parseConditional(condString) { // Parse syntax like: noDraftTodayMatching("tag") or noEventWithText("@(breathwork)") const match = condString.match(/^(\w+)\((.*?)\)$/); if (!match) return null; const funcName = match[1]; const argsString = match[2]; if (!CONDITIONS[funcName]) { log("Unknown condition function: " + funcName); return null; } // Parse arguments (handle quoted strings) const args = []; if (argsString) { const argMatches = argsString.match(/"([^"]*)"|'([^']*)'|(\d+)/g); if (argMatches) { argMatches.forEach(function(arg) { if (arg.startsWith('"') || arg.startsWith("'")) { args.push(arg.slice(1, -1)); // Remove quotes } else { args.push(parseInt(arg)); // Convert numbers } }); } } return { func: CONDITIONS[funcName], args: args }; } // ---------- URL SCHEME PARSERS ---------- function parseShortcutsURL(name) { return "shortcuts://run-shortcut?name=" + encodeURIComponent(name); } function parseDraftsURL(actionName) { if (_cache.actionURLByName[actionName]) return _cache.actionURLByName[actionName]; log("\n=== Drafts URL Parser ==="); log('Looking for action: "' + actionName + '"'); var action = Action.find(actionName); var url; if (action) { log('✓ Found action: "' + action.name + '"'); log(" UUID: " + action.uuid); // /runAction requires text= and action= expects the ACTION NAME url = "drafts://x-callback-url/runAction?text=%20&action=" + encodeURIComponent(action.name); } else { log("✗ Action not found"); url = "drafts://x-callback-url/runAction?text=%20&action=" + encodeURIComponent(actionName); } _cache.actionURLByName[actionName] = url; return url; } // ---------- LOAD LAUNCHER DATA ---------- const launcherDrafts = Draft.queryByTitle(LAUNCHER_DRAFT_TITLE); const launcherDraft = launcherDrafts && launcherDrafts.length ? launcherDrafts[0] : null; if (!launcherDraft) { alert("No launcher draft found. Please create a draft titled '" + LAUNCHER_DRAFT_TITLE + "'"); context.fail(); } // Source draft URL (open the launcher draft) const sourceDraftURL = "drafts://open?uuid=" + encodeURIComponent(launcherDraft.uuid); // ---------- PARSE MARKDOWN ---------- const lines = launcherDraft.content.split("\n"); const sections = []; let currentH1 = null; let currentH2 = null; const allTags = {}; const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/; const tagRegex = /#(\w+)/g; const conditionalRegex = /CONDITIONAL:\s*(.+?)(?:\s*$)/; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith("# ")) { currentH1 = line.substring(2).trim(); currentH2 = null; } else if (line.startsWith("## ")) { currentH2 = line.substring(3).trim(); sections.push({ h1: currentH1, h2: currentH2, items: [] }); } else if (currentH2 && (line.startsWith("+ [") || line.startsWith("- ["))) { const linkMatch = line.match(linkRegex); if (linkMatch) { const label = linkMatch[1]; let url = linkMatch[2]; // Handle shortcuts: pseudo-URLs if (url.startsWith("shortcuts:")) { const shortcutName = url.substring(10).trim(); url = parseShortcutsURL(shortcutName); } // Handle drafts: pseudo-URLs else if ((url.startsWith("drafts:") || url.startsWith("Drafts:")) && !url.startsWith("drafts://") && !url.startsWith("Drafts://")) { const actionName = url.substring(7).trim(); url = parseDraftsURL(actionName); } // Extract tags (but don't include them in conditional check) const tags = []; let tagMatch; const lineBeforeConditional = line.split("CONDITIONAL:")[0]; tagRegex.lastIndex = 0; while ((tagMatch = tagRegex.exec(lineBeforeConditional)) !== null) { const tag = tagMatch[1].toLowerCase(); tags.push(tag); allTags[tag] = (allTags[tag] || 0) + 1; } // Extract and evaluate conditional let shouldShow = true; const conditionalMatch = line.match(conditionalRegex); if (conditionalMatch) { const condString = conditionalMatch[1].trim(); const parsed = parseConditional(condString); if (parsed) { try { shouldShow = parsed.func.apply(null, parsed.args); log("Condition '" + condString + "' for '" + label + "': " + (shouldShow ? "SHOW" : "HIDE")); } catch(e) { log("Error evaluating condition '" + condString + "': " + e.message); shouldShow = true; // Show item on error } } } // Only add item if condition is met if (shouldShow) { sections[sections.length - 1].items.push({ label: label, url: url, tags: tags }); } else { log("Item '" + label + "' hidden by condition"); } } } } // Sort tags by frequency const sortedTags = Object.keys(allTags).sort(function(a, b) { return allTags[b] - allTags[a]; }); // ---------- HELPERS ---------- function esc(s) { return (s || "") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/'/g, "'") .replace(/"/g, """); } function isAppURL(url) { return !url.startsWith("http://") && !url.startsWith("https://"); } // ---------- CSS ---------- const css = ` *{box-sizing:border-box;margin:0;padding:0} body{background:#000;color:#fff;font-family:${TITLE_FONT};font-size:16px;line-height:1.6;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} ::selection{background:rgba(255,255,255,0.12)} .page{max-width:700px;margin:0 auto;padding:60px 40px 100px;min-height:100vh;position:relative} @media(max-width:640px){.page{padding:40px 30px 80px}} .close-btn{position:fixed;top:20px;right:20px;width:36px;height:36px;border-radius:50%;background:#1a1a1a;border:1px solid #333;color:#e06a70;font-size:20px;line-height:34px;text-align:center;cursor:pointer;transition:all .2s ease;z-index:1000} .close-btn:hover{background:#e06a70;color:#000;border-color:#e06a70;transform:rotate(90deg)} .source-btn{position:fixed;top:20px;right:64px;width:36px;height:36px;border-radius:50%;background:#1a1a1a;border:1px solid #333;color:#5AAE91;font-size:18px;line-height:34px;text-align:center;text-decoration:none;cursor:pointer;transition:all .2s ease;z-index:1000} .source-btn:hover{background:#5AAE91;color:#000;border-color:#5AAE91;transform:rotate(90deg)} .header{margin-bottom:30px;padding-bottom:20px;border-bottom:1px solid #444} .h1{font-size:18px;font-weight:500;letter-spacing:3px;text-transform:uppercase;margin-bottom:10px;color:#fff} .subtitle{font-size:13px;color:#888;font-style:italic;margin-bottom:20px} .search-container{position:sticky;top:0;background:#000;padding:16px 0 20px;z-index:100;border-bottom:1px solid #222;margin-bottom:20px} .search-bar{width:100%;padding:14px 18px;background:#0a0a0a;border:1px solid #333;border-radius:8px;color:#fff;font-size:15px;font-family:${TITLE_FONT};transition:all .2s ease} .search-bar:focus{outline:none;border-color:#5AAE91;background:#111} .search-bar::placeholder{color:#555} .tag-pills{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:30px} .tag-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:#0a0a0a;border:1px solid #222;border-radius:16px;font-size:12px;color:#888;cursor:pointer;transition:all .2s ease;font-family:${MONO_FONT}} .tag-pill:hover{border-color:#444;color:#aaa} .tag-pill.active{background:#5AAE91;border-color:#5AAE91;color:#000} .tag-count{opacity:0.6;font-size:11px} .section{margin-bottom:30px} .section.hidden{display:none} .section-header{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid #222;cursor:pointer;transition:border-color .2s ease} .section-header:hover{border-bottom-color:#444} .section-header.kb-selected{border-bottom-color:#5AAE91;background:rgba(90,174,145,0.1)} .section-title{font-size:15px;font-weight:500;letter-spacing:1px;text-transform:uppercase;color:#aaa} .section-count{font-size:13px;color:#555;font-family:${MONO_FONT}} .section-content{max-height:0;overflow:hidden;transition:max-height .3s ease} .section.open .section-content{max-height:2000px} .h1-label{font-size:11px;letter-spacing:1.2px;text-transform:uppercase;color:#666;margin-bottom:20px;padding-top:10px} .launcher-items{display:flex;flex-direction:column;gap:10px} .launcher-item{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#0a0a0a;border:1px solid #1a1a1a;border-radius:6px;cursor:pointer;transition:all .2s ease;text-decoration:none;color:#e0e0e0} .launcher-item:hover{background:#1a1a1a;border-color:#333;transform:translateX(4px)} .launcher-item.kb-selected{background:#1a1a1a;border-color:#5AAE91;transform:translateX(4px)} .launcher-item.hidden{display:none} .launcher-label{font-size:18px;font-weight:400;flex:1} .item-tags{display:flex;gap:6px;flex-wrap:wrap} .item-tag{font-size:10px;padding:3px 8px;background:#111;border:1px solid #222;border-radius:10px;color:#666;font-family:${MONO_FONT}} .no-results{text-align:center;padding:40px 20px;color:#666;font-style:italic;display:none} .no-results.visible{display:block} `; // ---------- BUILD HTML ---------- const totalItems = sections.reduce(function(sum, sec) { return sum + sec.items.length; }, 0); let htmlParts = ["<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'><style>" + css + "</style></head><body>\ <div class='page'>\ <div class='close-btn' onclick='Drafts.cancel()'>×</div>\ <a class='source-btn' href='" + esc(sourceDraftURL) + "' title='Open source draft'>≡</a>\ <div class='header'>\ <div class='h1'>Launcher</div>\ <div class='subtitle'>" + totalItems + " items · " + sections.length + " sections</div>\ <div class='search-container'>\ <input type='text' class='search-bar' id='searchBar' placeholder='Type to filter...' autocomplete='off' />\ </div>"]; // Tag pills if (sortedTags.length > 0) { htmlParts.push("<div class='tag-pills' id='tagPills'>"); sortedTags.forEach(function(tag) { const count = allTags[tag]; htmlParts.push("<div class='tag-pill' data-tag='" + esc(tag) + "' onclick='toggleTag(\"" + esc(tag) + "\")'>#" + esc(tag) + "<span class='tag-count'>" + count + "</span></div>"); }); htmlParts.push("</div>"); } htmlParts.push("</div>"); // close header // Group sections by H1 const h1Groups = {}; sections.forEach(function(sec) { if (!h1Groups[sec.h1]) h1Groups[sec.h1] = []; h1Groups[sec.h1].push(sec); }); const h1Keys = Object.keys(h1Groups); let firstSection = true; h1Keys.forEach(function(h1, h) { const secs = h1Groups[h1]; if (h1 && h1 !== "null") { htmlParts.push("<div class='h1-label'>" + esc(h1) + "</div>"); } secs.forEach(function(sec, s) { if (sec.items.length === 0) return; const secId = "sec-" + h + "-" + s; const openClass = firstSection ? " open" : ""; firstSection = false; htmlParts.push("<div class='section" + openClass + "' id='" + secId + "'>\ <div class='section-header' onclick='toggleSection(\"" + secId + "\")' data-section-id='" + secId + "'>\ <div class='section-title'>" + esc(sec.h2) + "</div>\ <div class='section-count'>" + sec.items.length + "</div>\ </div>\ <div class='section-content'>\ <div class='launcher-items'>"); sec.items.forEach(function(item, i) { const itemId = secId + "-item-" + i; const isApp = isAppURL(item.url); const tagsData = item.tags.join(" "); if (isApp) { htmlParts.push("<a href='" + esc(item.url) + "' class='launcher-item' id='" + itemId + "' "); } else { const escapedURL = item.url.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"'); htmlParts.push("<a href='#' class='launcher-item' id='" + itemId + "' onclick=\"event.preventDefault();openWebURL('" + escapedURL + "');return false;\" "); } htmlParts.push("data-label='" + esc(item.label).toLowerCase() + "' data-tags='" + esc(tagsData) + "' data-section='" + secId + "'>\ <div class='launcher-label'>" + esc(item.label) + "</div>"); if (item.tags.length > 0) { htmlParts.push("<div class='item-tags'>"); item.tags.forEach(function(tag) { htmlParts.push("<span class='item-tag'>#" + esc(tag) + "</span>"); }); htmlParts.push("</div>"); } htmlParts.push("</a>"); }); htmlParts.push("</div></div></div>"); }); }); htmlParts.push("<div class='no-results' id='noResults'>No matching items found</div>\ </div>\ <script>\ var activeTags=new Set();\ var selectedIndex=-1;\ var selectableElements=[];\ function updateSelectableElements(){selectableElements=[];var headers=document.querySelectorAll('.section-header');var items=document.querySelectorAll('.launcher-item:not(.hidden)');headers.forEach(function(h){if(h.closest('.section:not(.hidden)'))selectableElements.push(h);});items.forEach(function(i){selectableElements.push(i);});}\ function clearSelection(){selectableElements.forEach(function(el){el.classList.remove('kb-selected');});}\ function setSelection(index){clearSelection();if(index>=0&&index<selectableElements.length){selectedIndex=index;var el=selectableElements[selectedIndex];el.classList.add('kb-selected');el.scrollIntoView({block:'nearest',behavior:'smooth'});}}\ function moveSelection(delta){var newIndex=selectedIndex+delta;if(newIndex<0)newIndex=0;if(newIndex>=selectableElements.length)newIndex=selectableElements.length-1;setSelection(newIndex);}\ function activateSelected(){if(selectedIndex>=0&&selectedIndex<selectableElements.length){var el=selectableElements[selectedIndex];if(el.classList.contains('section-header')){var secId=el.getAttribute('data-section-id');if(secId)toggleSection(secId);}else{el.click();}}}\ function toggleSection(id){var sec=document.getElementById(id);if(sec){sec.classList.toggle('open');updateSelectableElements();}}\ function openWebURL(url){Drafts.send('launcherURL',JSON.stringify({url:url}));Drafts.continue();}\ function toggleTag(tag){var pill=document.querySelector('.tag-pill[data-tag=\"'+tag+'\"]');if(activeTags.has(tag)){activeTags.delete(tag);pill.classList.remove('active');}else{activeTags.add(tag);pill.classList.add('active');}filterItems();}\ var searchBar=document.getElementById('searchBar');\ var allItems=document.querySelectorAll('.launcher-item');\ var allSections=document.querySelectorAll('.section');\ var noResults=document.getElementById('noResults');\ function filterItems(){var query=searchBar.value.toLowerCase().trim();var hasVisibleItems=false;var sectionVisibility={};allItems.forEach(function(item){var label=item.getAttribute('data-label');var tags=item.getAttribute('data-tags');var sectionId=item.getAttribute('data-section');var matchesSearch=!query||label.indexOf(query)>=0;var matchesTags=activeTags.size===0||Array.from(activeTags).every(function(t){return tags.indexOf(t)>=0;});if(matchesSearch&&matchesTags){item.classList.remove('hidden');sectionVisibility[sectionId]=true;hasVisibleItems=true;}else{item.classList.add('hidden');}});allSections.forEach(function(sec){if(sectionVisibility[sec.id]){sec.classList.remove('hidden');sec.classList.add('open');}else{sec.classList.add('hidden');}});if(hasVisibleItems){noResults.classList.remove('visible');}else{noResults.classList.add('visible');}updateSelectableElements();clearSelection();selectedIndex=-1;}\ searchBar.addEventListener('input',filterItems);\ document.addEventListener('keydown',function(e){if(e.key==='ArrowDown'){e.preventDefault();if(document.activeElement===searchBar&&selectedIndex===-1){setSelection(0);}else{moveSelection(1);}}else if(e.key==='ArrowUp'){e.preventDefault();moveSelection(-1);}else if(e.key==='Enter'){if(document.activeElement!==searchBar){e.preventDefault();activateSelected();}}else if(e.key==='Tab'){if(document.activeElement!==searchBar){e.preventDefault();searchBar.focus();clearSelection();selectedIndex=-1;}}else if(e.key==='Escape'){Drafts.cancel();}});\ updateSelectableElements();\ setTimeout(function(){searchBar.focus();},100);\ </script>\ </body></html>"); const html = htmlParts.join(""); // ---------- SHOW PREVIEW ---------- const prev = HTMLPreview.create(); prev.prefersFullScreen = true; prev.hideInterface = true; prev.show(html); // ======================================== // PROCESSOR - Handle URL callbacks // ======================================== const msg = context.previewValues["launcherURL"]; if (msg) { try { const data = JSON.parse(msg); if (data.url) { app.openURL(data.url, false); } } catch(e) { alert("Error opening URL: " + e.message); } }
Options
-
After Success Default Notification Error Log Level Info