experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

start cmd overhaul

+1094 -521
+50 -321
app/cmd/commands.js
··· 1 + /** 2 + * Commands manager - loads and provides commands to the command panel 3 + */ 1 4 import { id, labels, schemas, storageKeys, defaults } from './config.js'; 2 5 import { openStore } from '../utils.js'; 3 - import windows from '../windows.js'; 6 + import commandsModule from './commands/index.js'; 4 7 5 8 console.log('commands'); 6 9 ··· 10 13 const store = openStore(id, defaults, clear /* clear storage */); 11 14 const api = window.app; 12 15 16 + // Command registry - uses an object map for legacy compatibility 13 17 let commands = {}; 14 18 15 - function onCommandsUpdated () { 19 + /** 20 + * Notifies the UI that commands have been updated 21 + */ 22 + function onCommandsUpdated() { 16 23 window.dispatchEvent(new CustomEvent('cmd-update-commands', { detail: commands })); 17 - console.log('main sending updated commands out', Object.keys(commands)) 24 + console.log('main sending updated commands out', Object.keys(commands)); 18 25 } 19 26 20 - window.addEventListener('DOMContentLoaded', initializeCommandSources); 21 - 22 - /* 23 - command is an object with two properties: 24 - 25 - - name: string label 26 - - execute: method 27 - 28 - TODO: 29 - - add canRun check - eg, switchContainer cannot run on about: urls 30 - - enable command generation at call time (instead of in advance) 31 - 32 - */ 27 + /** 28 + * Adds a command to the registry 29 + * @param {Object} command - The command object with name and execute properties 30 + */ 33 31 function addCommand(command) { 34 32 commands[command.name] = command; 35 33 onCommandsUpdated(); 36 34 } 37 35 36 + /** 37 + * Initializes all command sources and registers them 38 + */ 38 39 function initializeCommandSources() { 39 40 console.log('initializeCommandSources'); 40 41 41 - sourceOpenURL(); 42 - //sourceBookmarklets(); 43 - //sourceBookmark(); 44 - //sourceEmail(); 45 - //sourceGoogleDocs(); 46 - //sourceSendToWindow(); 47 - //sourceSwitchToWindow(); 48 - //sourceNewContainerTab(); 49 - //sourceSwitchTabContainer(); 50 - onCommandsUpdated(); 51 - } 52 - 53 - const sourceOpenURL = () => { 54 - // Add a regular open command 55 - addCommand({ 56 - name: 'open', 57 - execute: msg => { 58 - console.log('open command', msg); 59 - 60 - const parts = msg.typed.split(' '); 61 - parts.shift(); 62 - 63 - const address = parts.shift(); 64 - 65 - if (!address) { 66 - return; 67 - } 68 - 69 - // Use the new windows API 70 - windows.createWindow(address, { 71 - width: 800, 72 - height: 600, 73 - openDevTools: debug // Only open DevTools in debug mode 74 - }).then(windowController => { 75 - console.log('Window opened with ID:', windowController.id); 76 - }).catch(error => { 77 - console.error('Failed to open window:', error); 78 - }); 79 - 80 - return { 81 - command: 'open', 82 - address 83 - }; 84 - } 85 - }); 86 - 87 - // Add a debug command that opens windows with DevTools 88 - addCommand({ 89 - name: 'debug', 90 - execute: msg => { 91 - console.log('debug command', msg); 92 - 93 - const parts = msg.typed.split(' '); 94 - parts.shift(); 95 - 96 - const address = parts.shift(); 97 - 98 - if (!address) { 99 - return; 100 - } 101 - 102 - // Use the new windows API with DevTools enabled 103 - windows.createWindow(address, { 104 - width: 900, 105 - height: 700, 106 - openDevTools: true, 107 - detachedDevTools: true 108 - }).then(windowController => { 109 - console.log('Debug window opened with ID:', windowController.id); 110 - }).catch(error => { 111 - console.error('Failed to open debug window:', error); 112 - }); 113 - 114 - return { 115 - command: 'debug', 116 - address 117 - }; 118 - } 42 + // Load commands from the commands module 43 + const moduleCommands = commandsModule.commands; 44 + moduleCommands.forEach(command => { 45 + addCommand(command); 119 46 }); 120 47 121 - // Add a modal window command 122 - addCommand({ 123 - name: 'modal', 124 - execute: msg => { 125 - console.log('modal command', msg); 126 - 127 - const parts = msg.typed.split(' '); 128 - parts.shift(); 48 + // Initialize any command sources that dynamically generate commands 49 + if (typeof commandsModule.initializeSources === 'function') { 50 + commandsModule.initializeSources(addCommand); 51 + } 129 52 130 - const address = parts.shift(); 131 - 132 - if (!address) { 133 - return; 134 - } 135 - 136 - // Use the modal window API 137 - windows.openModalWindow(address, { 138 - width: 700, 139 - height: 500 140 - }).then(result => { 141 - console.log('Modal window opened:', result); 142 - }).catch(error => { 143 - console.error('Failed to open modal window:', error); 144 - }); 145 - 146 - return { 147 - command: 'modal', 148 - address 149 - }; 150 - } 151 - }); 53 + // Notify that commands are ready 54 + onCommandsUpdated(); 152 55 } 153 56 154 - /* 155 - async function sourceBookmarklets() { 156 - // add bookmarklets as commands 157 - let bmarklets = await browser.bookmarks.search({ query: 'javascript:'} ); 158 - bmarklets.map(b => { 159 - return { 160 - name: b.title, 161 - async execute(cmd) { 162 - //let tags = cmd.typed.split(' ').filter(w => w != cmd.name) 163 - //console.log('tags', tags) 164 - let tabs = await browser.tabs.query({active:true}); 165 - browser.tabs.executeScript(tabs[0].id, { 166 - code: b.url.replace('javascript:', '') 167 - }); 168 - } 169 - }; 170 - }).forEach(addCommand); 171 - } 172 - */ 57 + // Initialize commands when the DOM is loaded 58 + window.addEventListener('DOMContentLoaded', initializeCommandSources); 173 59 174 - /* 175 - async function sourceBookmark() { 176 - addCommand({ 177 - name: 'bookmark current page', 178 - async execute() { 179 - let tab = await browser.tabs.query({active:true}); 180 - let node = await browser.bookmarks.create({ 181 - title: tab[0].title, 182 - url: tab[0].url 183 - }); 184 - } 185 - }); 186 - } 187 - */ 188 - 189 - /* 190 - // FIXME 191 - async function sourceEmail() { 192 - addCommand({ 193 - name: 'Email page to', 194 - async execute(msg) { 195 - let tabs = await browser.tabs.query({active:true}); 196 - let email = msg.typed.replace(msg.name, '').trim(); 197 - let url = 198 - 'mailto:' + email + 199 - '?subject=Web%20page!&body=' + 200 - encodeURIComponent(tabs[0].title) + 201 - '%0D%0A' + 202 - encodeURIComponent(tabs[0].url); 203 - tabs[0].url = url; 204 - } 205 - }); 206 - } 207 - */ 208 - 209 - /* 210 - async function sourceGoogleDocs() { 211 - [ 212 - { 213 - cmd: 'New Google doc', 214 - url: 'http://docs.google.com/document/create?hl=en' 215 - }, 216 - { 217 - cmd: 'New Google sheet', 218 - url: 'http://spreadsheets.google.com/ccc?new&hl=en' 219 - } 220 - ].forEach(function(doc) { 221 - addCommand({ 222 - name: doc.cmd, 223 - async execute(msg) { 224 - await browser.tabs.create({ 225 - url: doc.url 226 - }); 227 - } 60 + /** 61 + * Helper function for notifications (currently unused) 62 + */ 63 + function notify(title, content) { 64 + if (typeof browser !== 'undefined' && browser.notifications) { 65 + browser.notifications.create({ 66 + "type": "basic", 67 + "iconUrl": browser.extension.getURL("images/icon.png"), 68 + "title": title, 69 + "message": content 228 70 }); 229 - }); 71 + } else { 72 + console.log('Notification:', title, content); 73 + } 230 74 } 231 - */ 232 75 233 76 /* 234 - async function sourceSendToWindow() { 235 - const cmdPrefix = 'Move to window: '; 236 - const windows = await browser.windows.getAll({windowTypes: ['normal']}); 237 - windows.forEach((w) => { 238 - addCommand({ 239 - name: cmdPrefix + w.title, 240 - async execute(msg) { 241 - const activeTabs = await browser.tabs.query({active: true}); 242 - browser.tabs.move(activeTabs[0].id, {windowId: w.id, index: -1}); 243 - } 244 - }); 245 - }); 246 - } 247 - */ 248 - 249 - /* 250 - async function sourceSwitchToWindow() { 251 - const cmdPrefix = 'Switch to window: '; 252 - const windows = await browser.windows.getAll({}); 253 - windows.forEach((w) => { 254 - addCommand({ 255 - name: cmdPrefix + w.title, 256 - async execute(msg) { 257 - browser.windows.update(w.id, { focused: true }); 258 - } 259 - }); 260 - }); 261 - } 262 - */ 263 - 264 - /* 265 - async function sourceNewContainerTab() { 266 - const cmdPrefix = 'New container tab: '; 267 - browser.contextualIdentities.query({}) 268 - .then((identities) => { 269 - if (!identities.length) 270 - return; 271 - for (let identity of identities) { 272 - addCommand({ 273 - name: cmdPrefix + identity.name, 274 - async execute(msg) { 275 - browser.tabs.create({url: '', cookieStoreId: identity.cookieStoreId }); 276 - } 277 - }); 278 - } 279 - }); 280 - } 281 - */ 282 - 283 - /* 284 - async function sourceSwitchTabContainer() { 285 - const cmdPrefix = 'Switch container to: '; 286 - browser.contextualIdentities.query({}) 287 - .then((identities) => { 288 - if (!identities.length) 289 - return; 290 - for (let identity of identities) { 291 - addCommand({ 292 - name: cmdPrefix + identity.name, 293 - async execute(msg) { 294 - const activeTabs = await browser.tabs.query({currentWindow: true, active: true}); 295 - const tab = activeTabs[0]; 296 - // some risk of losing old tab if new tab was not created successfully 297 - // but putting remove in creation was getting killed by window close 298 - // so when execution is moved to background script, try moving this back 299 - browser.tabs.remove(tab.id); 300 - browser.tabs.create({url: tab.url, cookieStoreId: identity.cookieStoreId, index: tab.index+1, pinned: tab.pinned }).then(() => { 301 - // tab remove should be here 302 - }); 303 - } 304 - }); 305 - } 306 - }); 307 - } 308 - */ 309 - 310 - /* 311 - async function sourceNote() { 312 - addCommand({ 313 - name: 'note', 314 - async execute(msg) { 315 - console.log('note execd', msg) 316 - if (msg.typed.indexOf(' ')) { 317 - let note = msg.typed.replace('note ', ''); 318 - await saveNewNote(note) 319 - notify('note saved!', note) 320 - } 321 - } 322 - }); 323 - 324 - const STG_KEY = 'cmd:notes'; 325 - const STG_TYPE = 'local'; 326 - 327 - async function saveNewNote(note) { 328 - let store = await browser.storage[STG_TYPE].get(STG_KEY) 329 - console.log('store', store) 330 - if (Object.keys(store).indexOf(STG_KEY) == -1) { 331 - console.log('new store') 332 - store = { 333 - notes: [] 334 - } 335 - } 336 - else { 337 - store = store[STG_KEY] 338 - } 339 - store.notes.push(note) 340 - 341 - await browser.storage[STG_TYPE].set({ [STG_KEY] : store}) 342 - console.log('saved store', store); 343 - } 344 - } 345 - await sourceNote() 346 - */ 347 - 348 - function notify(title, content) { 349 - browser.notifications.create({ 350 - "type": "basic", 351 - "iconUrl": browser.extension.getURL("images/icon.png"), 352 - "title": title, 353 - "message": content 354 - }); 355 - } 77 + * The following commented-out functions represent command sources 78 + * that were previously defined in this file. They've been moved to 79 + * individual module files in the commands/ directory. 80 + * 81 + * If you need to re-enable any of these command sources, please 82 + * create a new module file for each in the commands/ directory 83 + * and update the commands/index.js file accordingly. 84 + */
+39
app/cmd/commands/bookmark.js
··· 1 + /** 2 + * Bookmark command - bookmarks the current page 3 + * Currently disabled, needs browser extension API 4 + */ 5 + 6 + export default { 7 + name: 'bookmark current page', 8 + 9 + /** 10 + * Executes the bookmark command 11 + */ 12 + execute: async () => { 13 + if (typeof browser === 'undefined' || !browser.tabs || !browser.bookmarks) { 14 + console.error('Bookmark command disabled: browser API not available'); 15 + return { success: false, error: 'Browser API not available' }; 16 + } 17 + 18 + try { 19 + let tab = await browser.tabs.query({active:true}); 20 + let node = await browser.bookmarks.create({ 21 + title: tab[0].title, 22 + url: tab[0].url 23 + }); 24 + 25 + return { 26 + success: true, 27 + command: 'bookmark', 28 + bookmark: { 29 + id: node.id, 30 + title: node.title, 31 + url: node.url 32 + } 33 + }; 34 + } catch (error) { 35 + console.error('Failed to bookmark page:', error); 36 + return { success: false, error: error.message }; 37 + } 38 + } 39 + };
+42
app/cmd/commands/bookmarklets.js
··· 1 + /** 2 + * Bookmarklets command - adds bookmarklets as commands 3 + * Currently disabled, needs browser extension API 4 + */ 5 + 6 + export default { 7 + name: 'bookmarklets-source', 8 + type: 'source', 9 + 10 + /** 11 + * Initializes and registers bookmarklet commands 12 + * @param {Function} addCommand - Function to register a command 13 + */ 14 + initialize: async (addCommand) => { 15 + if (typeof browser === 'undefined' || !browser.bookmarks) { 16 + console.log('Bookmarklets source disabled: browser bookmarks API not available'); 17 + return; 18 + } 19 + 20 + try { 21 + // add bookmarklets as commands 22 + let bmarklets = await browser.bookmarks.search({ query: 'javascript:'} ); 23 + bmarklets.map(b => { 24 + return { 25 + name: b.title, 26 + async execute(cmd) { 27 + //let tags = cmd.typed.split(' ').filter(w => w != cmd.name) 28 + //console.log('tags', tags) 29 + let tabs = await browser.tabs.query({active:true}); 30 + browser.tabs.executeScript(tabs[0].id, { 31 + code: b.url.replace('javascript:', '') 32 + }); 33 + } 34 + }; 35 + }).forEach(addCommand); 36 + 37 + console.log('Registered bookmarklet commands:', bmarklets.length); 38 + } catch (error) { 39 + console.error('Failed to initialize bookmarklets source:', error); 40 + } 41 + } 42 + };
+95
app/cmd/commands/containertab.js
··· 1 + /** 2 + * Container Tab commands - creates and manages container tabs 3 + * Currently disabled, needs browser extension API 4 + */ 5 + 6 + export default { 7 + name: 'containertab-source', 8 + type: 'source', 9 + 10 + /** 11 + * Initializes and registers container tab commands 12 + * @param {Function} addCommand - Function to register a command 13 + */ 14 + initialize: async (addCommand) => { 15 + if (typeof browser === 'undefined' || !browser.contextualIdentities || !browser.tabs) { 16 + console.log('Container Tab source disabled: browser contextualIdentities API not available'); 17 + return; 18 + } 19 + 20 + try { 21 + // Initialize "New container tab" commands 22 + const newCmdPrefix = 'New container tab: '; 23 + const switchCmdPrefix = 'Switch container to: '; 24 + 25 + const identities = await browser.contextualIdentities.query({}); 26 + 27 + if (!identities.length) { 28 + console.log('No container identities found'); 29 + return; 30 + } 31 + 32 + // Register "New container tab" commands 33 + for (let identity of identities) { 34 + // Command to create a new tab in a container 35 + addCommand({ 36 + name: newCmdPrefix + identity.name, 37 + async execute(msg) { 38 + try { 39 + await browser.tabs.create({ 40 + url: '', 41 + cookieStoreId: identity.cookieStoreId 42 + }); 43 + return { 44 + success: true, 45 + command: 'newcontainertab', 46 + container: identity.name 47 + }; 48 + } catch (error) { 49 + console.error('Failed to create container tab:', error); 50 + return { success: false, error: error.message }; 51 + } 52 + } 53 + }); 54 + 55 + // Command to switch the current tab to a different container 56 + addCommand({ 57 + name: switchCmdPrefix + identity.name, 58 + async execute(msg) { 59 + try { 60 + const activeTabs = await browser.tabs.query({ 61 + currentWindow: true, 62 + active: true 63 + }); 64 + const tab = activeTabs[0]; 65 + 66 + // Create a new tab in the target container with the same URL 67 + await browser.tabs.create({ 68 + url: tab.url, 69 + cookieStoreId: identity.cookieStoreId, 70 + index: tab.index+1, 71 + pinned: tab.pinned 72 + }); 73 + 74 + // Remove the original tab 75 + browser.tabs.remove(tab.id); 76 + 77 + return { 78 + success: true, 79 + command: 'switchcontainer', 80 + container: identity.name 81 + }; 82 + } catch (error) { 83 + console.error('Failed to switch container:', error); 84 + return { success: false, error: error.message }; 85 + } 86 + } 87 + }); 88 + } 89 + 90 + console.log('Registered Container Tab commands:', identities.length * 2); 91 + } catch (error) { 92 + console.error('Failed to initialize Container Tab source:', error); 93 + } 94 + } 95 + };
+38
app/cmd/commands/debug.js
··· 1 + /** 2 + * Debug command - opens a URL in a new window with DevTools enabled 3 + */ 4 + import windows from '../../windows.js'; 5 + 6 + export default { 7 + name: 'debug', 8 + execute: async (msg) => { 9 + console.log('debug command', msg); 10 + 11 + const parts = msg.typed.split(' '); 12 + parts.shift(); 13 + 14 + const address = parts.shift(); 15 + 16 + if (!address) { 17 + return; 18 + } 19 + 20 + // Use the new windows API with DevTools enabled 21 + try { 22 + const windowController = await windows.createWindow(address, { 23 + width: 900, 24 + height: 700, 25 + openDevTools: true, 26 + detachedDevTools: true 27 + }); 28 + console.log('Debug window opened with ID:', windowController.id); 29 + } catch (error) { 30 + console.error('Failed to open debug window:', error); 31 + } 32 + 33 + return { 34 + command: 'debug', 35 + address 36 + }; 37 + } 38 + };
+42
app/cmd/commands/email.js
··· 1 + /** 2 + * Email command - emails the current page 3 + * Currently disabled, needs browser extension API 4 + */ 5 + 6 + export default { 7 + name: 'Email page to', 8 + 9 + /** 10 + * Executes the email command 11 + */ 12 + execute: async (msg) => { 13 + if (typeof browser === 'undefined' || !browser.tabs) { 14 + console.error('Email command disabled: browser API not available'); 15 + return { success: false, error: 'Browser API not available' }; 16 + } 17 + 18 + try { 19 + let tabs = await browser.tabs.query({active:true}); 20 + let email = msg.typed.replace(msg.name, '').trim(); 21 + let url = 22 + 'mailto:' + email + 23 + '?subject=Web%20page!&body=' + 24 + encodeURIComponent(tabs[0].title) + 25 + '%0D%0A' + 26 + encodeURIComponent(tabs[0].url); 27 + 28 + // Navigate the current tab to the mailto: URL 29 + // Note: This approach might be replaced with a more modern API 30 + tabs[0].url = url; 31 + 32 + return { 33 + success: true, 34 + command: 'email', 35 + recipient: email 36 + }; 37 + } catch (error) { 38 + console.error('Failed to email page:', error); 39 + return { success: false, error: error.message }; 40 + } 41 + } 42 + };
+52
app/cmd/commands/googledocs.js
··· 1 + /** 2 + * Google Docs commands - creates new Google Docs documents 3 + * Currently disabled, needs browser extension API 4 + */ 5 + 6 + export default { 7 + name: 'googledocs-source', 8 + type: 'source', 9 + 10 + /** 11 + * Initializes and registers Google Docs commands 12 + * @param {Function} addCommand - Function to register a command 13 + */ 14 + initialize: (addCommand) => { 15 + if (typeof browser === 'undefined' || !browser.tabs) { 16 + console.log('Google Docs source disabled: browser tabs API not available'); 17 + return; 18 + } 19 + 20 + // Define the available document types 21 + const documents = [ 22 + { 23 + cmd: 'New Google doc', 24 + url: 'http://docs.google.com/document/create?hl=en' 25 + }, 26 + { 27 + cmd: 'New Google sheet', 28 + url: 'http://spreadsheets.google.com/ccc?new&hl=en' 29 + } 30 + ]; 31 + 32 + // Register each document type as a command 33 + documents.forEach(function(doc) { 34 + addCommand({ 35 + name: doc.cmd, 36 + async execute(msg) { 37 + try { 38 + await browser.tabs.create({ 39 + url: doc.url 40 + }); 41 + return { success: true, command: doc.cmd }; 42 + } catch (error) { 43 + console.error(`Failed to create ${doc.cmd}:`, error); 44 + return { success: false, error: error.message }; 45 + } 46 + } 47 + }); 48 + }); 49 + 50 + console.log('Registered Google Docs commands:', documents.length); 51 + } 52 + };
+98
app/cmd/commands/index.js
··· 1 + /** 2 + * Commands module - exports all available commands 3 + */ 4 + import openCommand from './open.js'; 5 + import debugCommand from './debug.js'; 6 + import modalCommand from './modal.js'; 7 + 8 + // Source commands (commented out as they need browser extension APIs) 9 + // These modules contain command sources that dynamically generate commands 10 + import bookmarkletsSource from './bookmarklets.js'; 11 + import googleDocsSource from './googledocs.js'; 12 + import sendToWindowSource from './sendtowindow.js'; 13 + import switchToWindowSource from './switchtowindow.js'; 14 + import containerTabSource from './containertab.js'; 15 + 16 + // Individual commands (commented out as they need browser extension APIs) 17 + import bookmarkCommand from './bookmark.js'; 18 + import emailCommand from './email.js'; 19 + import noteCommand from './note.js'; 20 + 21 + // Active commands - only these will be loaded 22 + const activeCommands = [ 23 + openCommand, 24 + debugCommand, 25 + modalCommand 26 + ]; 27 + 28 + // Inactive commands - these require browser extension APIs and are not loaded 29 + const inactiveCommands = [ 30 + // Individual commands 31 + bookmarkCommand, 32 + emailCommand, 33 + noteCommand, 34 + 35 + // Source commands that dynamically generate commands 36 + bookmarkletsSource, 37 + googleDocsSource, 38 + sendToWindowSource, 39 + switchToWindowSource, 40 + containerTabSource 41 + ]; 42 + 43 + // Array of all available commands 44 + const commands = [...activeCommands]; 45 + 46 + // Source commands - these are modules that generate multiple commands 47 + const sources = []; 48 + 49 + /** 50 + * Initializes command sources that dynamically generate commands 51 + * @param {Function} addCommand - Function to register a command 52 + */ 53 + export const initializeSources = (addCommand) => { 54 + // Currently no active sources 55 + sources.forEach(source => { 56 + if (typeof source.initialize === 'function') { 57 + source.initialize(addCommand); 58 + } 59 + }); 60 + }; 61 + 62 + /** 63 + * Gets a command by name 64 + * @param {string} name - The command name to look for 65 + * @returns {Object|null} The command object or null if not found 66 + */ 67 + export const getCommand = (name) => { 68 + return commands.find(cmd => cmd.name === name) || null; 69 + }; 70 + 71 + /** 72 + * Gets all active commands 73 + * @returns {Array} Array of command objects 74 + */ 75 + export const getAllCommands = () => { 76 + return commands; 77 + }; 78 + 79 + /** 80 + * Converts commands array to object map for legacy compatibility 81 + * @returns {Object} Map of command name to command object 82 + */ 83 + export const getCommandsMap = () => { 84 + const commandMap = {}; 85 + commands.forEach(cmd => { 86 + commandMap[cmd.name] = cmd; 87 + }); 88 + return commandMap; 89 + }; 90 + 91 + export default { 92 + commands, 93 + sources, 94 + getCommand, 95 + getAllCommands, 96 + getCommandsMap, 97 + initializeSources 98 + };
+36
app/cmd/commands/modal.js
··· 1 + /** 2 + * Modal command - opens a URL in a modal window that hides on blur or escape 3 + */ 4 + import windows from '../../windows.js'; 5 + 6 + export default { 7 + name: 'modal', 8 + execute: async (msg) => { 9 + console.log('modal command', msg); 10 + 11 + const parts = msg.typed.split(' '); 12 + parts.shift(); 13 + 14 + const address = parts.shift(); 15 + 16 + if (!address) { 17 + return; 18 + } 19 + 20 + // Use the modal window API 21 + try { 22 + const result = await windows.openModalWindow(address, { 23 + width: 700, 24 + height: 500 25 + }); 26 + console.log('Modal window opened:', result); 27 + } catch (error) { 28 + console.error('Failed to open modal window:', error); 29 + } 30 + 31 + return { 32 + command: 'modal', 33 + address 34 + }; 35 + } 36 + };
+86
app/cmd/commands/note.js
··· 1 + /** 2 + * Note command - saves a note 3 + * Currently disabled, needs browser extension API 4 + */ 5 + 6 + export default { 7 + name: 'note', 8 + 9 + /** 10 + * Executes the note command 11 + */ 12 + execute: async (msg) => { 13 + if (typeof browser === 'undefined' || !browser.storage) { 14 + console.error('Note command disabled: browser storage API not available'); 15 + return { success: false, error: 'Browser API not available' }; 16 + } 17 + 18 + console.log('note executed', msg); 19 + 20 + try { 21 + if (msg.typed.indexOf(' ') !== -1) { 22 + const noteText = msg.typed.replace('note ', ''); 23 + await saveNewNote(noteText); 24 + 25 + // Notify user 26 + if (typeof notify === 'function') { 27 + notify('Note saved!', noteText); 28 + } else { 29 + console.log('Note saved:', noteText); 30 + } 31 + 32 + return { success: true, command: 'note', text: noteText }; 33 + } else { 34 + return { success: false, error: 'No note text provided' }; 35 + } 36 + } catch (error) { 37 + console.error('Failed to save note:', error); 38 + return { success: false, error: error.message }; 39 + } 40 + } 41 + }; 42 + 43 + // Storage constants 44 + const STG_KEY = 'cmd:notes'; 45 + const STG_TYPE = 'local'; 46 + 47 + /** 48 + * Saves a new note to browser storage 49 + * @param {string} note - The note text to save 50 + */ 51 + async function saveNewNote(note) { 52 + let store = await browser.storage[STG_TYPE].get(STG_KEY); 53 + console.log('store', store); 54 + 55 + if (Object.keys(store).indexOf(STG_KEY) === -1) { 56 + console.log('new store'); 57 + store = { 58 + notes: [] 59 + }; 60 + } else { 61 + store = store[STG_KEY]; 62 + } 63 + 64 + store.notes.push(note); 65 + 66 + await browser.storage[STG_TYPE].set({ [STG_KEY]: store }); 67 + console.log('saved store', store); 68 + } 69 + 70 + /** 71 + * Notifies the user 72 + * @param {string} title - Notification title 73 + * @param {string} content - Notification content 74 + */ 75 + function notify(title, content) { 76 + if (browser && browser.notifications) { 77 + browser.notifications.create({ 78 + "type": "basic", 79 + "iconUrl": browser.extension.getURL("images/icon.png"), 80 + "title": title, 81 + "message": content 82 + }); 83 + } else { 84 + console.log('Notification:', title, content); 85 + } 86 + }
+93
app/cmd/commands/open.js
··· 1 + /** 2 + * Open command - opens a URL in a new window 3 + * Only opens window if input is a valid URL 4 + */ 5 + import windows from '../../windows.js'; 6 + 7 + export default { 8 + name: 'open', 9 + execute: async (msg) => { 10 + console.log('open command', msg); 11 + 12 + const parts = msg.typed.split(' '); 13 + parts.shift(); 14 + 15 + const address = parts.shift(); 16 + 17 + if (!address) { 18 + console.log('No address provided'); 19 + return { error: 'No address provided' }; 20 + } 21 + 22 + // Check if the input is a valid URL and get the normalized version 23 + const urlResult = getValidURL(address); 24 + if (!urlResult.valid) { 25 + console.log('Invalid URL:', address); 26 + return { error: 'Invalid URL. Must be a valid URL starting with http://, https://, or other valid protocol.' }; 27 + } 28 + 29 + // Use the normalized URL (with protocol added if needed) 30 + const normalizedAddress = urlResult.url; 31 + console.log('Using normalized URL:', normalizedAddress); 32 + 33 + // Use the new windows API 34 + try { 35 + const windowController = await windows.createWindow(normalizedAddress, { 36 + width: 800, 37 + height: 600, 38 + openDevTools: window.app.debug // Only open DevTools in debug mode 39 + }); 40 + console.log('Window opened with ID:', windowController.id); 41 + 42 + return { 43 + command: 'open', 44 + address: normalizedAddress, 45 + success: true 46 + }; 47 + } catch (error) { 48 + console.error('Failed to open window:', error); 49 + return { 50 + error: 'Failed to open window: ' + error.message, 51 + address: normalizedAddress 52 + }; 53 + } 54 + } 55 + }; 56 + 57 + /** 58 + * Validates and normalizes a URL string 59 + * @param {string} str - The string to check 60 + * @returns {Object} - Object with valid flag and normalized URL 61 + */ 62 + function getValidURL(str) { 63 + // Quick check for empty string 64 + if (!str) return { valid: false }; 65 + 66 + // Check if it starts with a valid protocol 67 + const hasValidProtocol = /^(https?|ftp|file|peek):\/\//.test(str); 68 + 69 + if (!hasValidProtocol) { 70 + // If no protocol, check if it's a domain name pattern 71 + const isDomainPattern = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(str); 72 + if (isDomainPattern) { 73 + // It's a domain without protocol, add https:// 74 + const urlWithProtocol = 'https://' + str; 75 + try { 76 + // Validate the URL with added protocol 77 + new URL(urlWithProtocol); 78 + return { valid: true, url: urlWithProtocol }; 79 + } catch (e) { 80 + return { valid: false }; 81 + } 82 + } 83 + return { valid: false }; 84 + } 85 + 86 + try { 87 + // Already has protocol, just validate 88 + new URL(str); 89 + return { valid: true, url: str }; 90 + } catch (e) { 91 + return { valid: false }; 92 + } 93 + }
+45
app/cmd/commands/sendtowindow.js
··· 1 + /** 2 + * Send to Window command - moves the current tab to another window 3 + * Currently disabled, needs browser extension API 4 + */ 5 + 6 + export default { 7 + name: 'sendtowindow-source', 8 + type: 'source', 9 + 10 + /** 11 + * Initializes and registers Send to Window commands 12 + * @param {Function} addCommand - Function to register a command 13 + */ 14 + initialize: async (addCommand) => { 15 + if (typeof browser === 'undefined' || !browser.windows || !browser.tabs) { 16 + console.log('Send to Window source disabled: browser windows/tabs API not available'); 17 + return; 18 + } 19 + 20 + try { 21 + const cmdPrefix = 'Move to window: '; 22 + const windows = await browser.windows.getAll({windowTypes: ['normal']}); 23 + 24 + windows.forEach((w) => { 25 + addCommand({ 26 + name: cmdPrefix + w.title, 27 + async execute(msg) { 28 + try { 29 + const activeTabs = await browser.tabs.query({active: true}); 30 + await browser.tabs.move(activeTabs[0].id, {windowId: w.id, index: -1}); 31 + return { success: true, command: 'movetowindow', windowId: w.id }; 32 + } catch (error) { 33 + console.error('Failed to move tab to window:', error); 34 + return { success: false, error: error.message }; 35 + } 36 + } 37 + }); 38 + }); 39 + 40 + console.log('Registered Send to Window commands:', windows.length); 41 + } catch (error) { 42 + console.error('Failed to initialize Send to Window source:', error); 43 + } 44 + } 45 + };
+44
app/cmd/commands/switchtowindow.js
··· 1 + /** 2 + * Switch to Window command - focuses another window 3 + * Currently disabled, needs browser extension API 4 + */ 5 + 6 + export default { 7 + name: 'switchtowindow-source', 8 + type: 'source', 9 + 10 + /** 11 + * Initializes and registers Switch to Window commands 12 + * @param {Function} addCommand - Function to register a command 13 + */ 14 + initialize: async (addCommand) => { 15 + if (typeof browser === 'undefined' || !browser.windows) { 16 + console.log('Switch to Window source disabled: browser windows API not available'); 17 + return; 18 + } 19 + 20 + try { 21 + const cmdPrefix = 'Switch to window: '; 22 + const windows = await browser.windows.getAll({}); 23 + 24 + windows.forEach((w) => { 25 + addCommand({ 26 + name: cmdPrefix + w.title, 27 + async execute(msg) { 28 + try { 29 + await browser.windows.update(w.id, { focused: true }); 30 + return { success: true, command: 'switchtowindow', windowId: w.id }; 31 + } catch (error) { 32 + console.error('Failed to switch to window:', error); 33 + return { success: false, error: error.message }; 34 + } 35 + } 36 + }); 37 + }); 38 + 39 + console.log('Registered Switch to Window commands:', windows.length); 40 + } catch (error) { 41 + console.error('Failed to initialize Switch to Window source:', error); 42 + } 43 + } 44 + };
+33
app/cmd/commands/template.js
··· 1 + /** 2 + * Template command - use this as a starting point for new commands 3 + * 4 + * To create a new command: 5 + * 1. Copy this file to a new file named after your command (e.g., mycommand.js) 6 + * 2. Update the name and execute function 7 + * 3. Import the command in index.js and add it to the commands array 8 + */ 9 + 10 + export default { 11 + // Command name - what the user will type to execute this command 12 + name: 'template', 13 + 14 + // Execute function - called when the command is selected 15 + execute: async (msg) => { 16 + console.log('template command executed', msg); 17 + 18 + // Parse any arguments from the command 19 + const parts = msg.typed.split(' '); 20 + parts.shift(); // Remove the command name 21 + 22 + const args = parts.join(' '); 23 + 24 + // Implement your command logic here 25 + 26 + // Return a result object 27 + return { 28 + command: 'template', 29 + success: true, 30 + // Add other properties as needed 31 + }; 32 + } 33 + };
+40 -6
app/cmd/panel.html
··· 29 29 box-sizing: border-box; 30 30 } 31 31 32 + /* Command display wrapper */ 33 + .command-display { 34 + width: 100%; 35 + height: 40px; 36 + position: relative; 37 + } 38 + 39 + /* Command input field */ 32 40 #command-input { 33 41 width: 100%; 34 42 height: 40px; ··· 40 48 font-weight: 500; 41 49 padding: 0; 42 50 -webkit-app-region: no-drag; 51 + position: relative; 52 + z-index: 2; 43 53 } 44 54 55 + /* Hidden suggestion element that shows the matching command with highlighting */ 56 + #command-text { 57 + position: absolute; 58 + top: 0; 59 + left: 0; 60 + width: 100%; 61 + height: 40px; 62 + line-height: 40px; 63 + font-size: 20px; 64 + font-weight: 500; 65 + color: rgba(255, 255, 255, 0.5); 66 + white-space: pre; 67 + overflow: hidden; 68 + pointer-events: none; 69 + z-index: 1; 70 + } 71 + 72 + /* Styling for the matched part */ 73 + #command-text .matched { 74 + text-decoration: underline; 75 + } 76 + 77 + /* Results list */ 45 78 #results { 46 79 display: none; 47 - position: absolute; 48 - top: 60%; 49 - left: 10px; 50 - right: 10px; 80 + margin-top: 5px; 81 + width: 100%; 51 82 max-height: 200px; 52 83 overflow-y: auto; 53 84 -webkit-app-region: no-drag; ··· 58 89 } 59 90 60 91 .command-item { 61 - padding: 5px; 92 + padding: 8px; 62 93 cursor: pointer; 63 94 border-radius: 4px; 64 95 } ··· 74 105 </head> 75 106 <body> 76 107 <div class="center-wrapper"> 77 - <input id="command-input" type="text" autofocus placeholder="Type a command..." /> 108 + <div class="command-display"> 109 + <input id="command-input" type="text" autofocus spellcheck="false" /> 110 + <div id="command-text"></div> 111 + </div> 78 112 </div> 79 113 <div id="results"></div> 80 114
+261 -194
app/cmd/panel.js
··· 1 1 // cmd/panel.js 2 - /* 3 - 4 - TODO: NOW 5 - * multistring search, eg "new pl" matches "new container tab: PL" 6 - * <tab> to move to next in list (figure out vs params, chaining, etc) 7 - * store state data in add-on, not localStorage 8 - * placeholder text not working in release 9 - * fix default command 10 - * move command execution to background script 11 - 12 - TODO: NEXT 13 - * command suggestions (listed below - eg, see windows) 14 - * command parameters 15 - * command screenshots (eg, switch to window) 16 - * command chaining 17 - 18 - TODO: FUTURE 19 - * remember last-executed command across restarts 20 - * better visual fix for overflow text 21 - * commands that identify things in the page and act on them (locations, events, people) 22 - 23 - TODO: Settings 24 - * add settings to right corner 25 - * settings page 26 - * configurable shortcut 27 - 28 - TODO: Long running jobs 29 - * add support for long-running jobs 30 - * add support for "log in to <svc>" 31 - * add notifications to right corner 32 - 33 - TODO: Commands 34 - * switch to window command, searching by title (named windows?) 35 - * IPFS 36 - * Flickr 37 - * Pocket 38 - 39 - */ 40 - 41 2 import { id, labels, schemas, storageKeys, defaults } from './config.js'; 42 3 import { openStore } from "../utils.js"; 43 4 ··· 50 11 const api = window.app; 51 12 52 13 const address = 'peek://cmd/panel.html'; 53 - 54 14 55 15 let state = { 56 16 commands: [], // array of command names 57 17 matches: [], // array of commands matching the typed text 58 - matchIndex: 0, // index of ??? 18 + matchIndex: 0, // index of selected match 59 19 matchCounts: {}, // match counts - selectedcommand:numberofselections 60 20 matchFeedback: {}, // adaptive matching - partiallytypedandselected:fullname 61 21 typed: '', // text typed by user so far, if any ··· 63 23 }; 64 24 65 25 window.addEventListener('cmd-update-commands', function(e) { 66 - console.log('ui received updated commands'); 26 + debug && console.log('ui received updated commands'); 67 27 state.commands = e.detail; 68 28 }); 69 29 70 30 async function render() { 71 - // Get the command input element and results container 31 + // Get elements 72 32 const commandInput = document.getElementById('command-input'); 33 + const commandText = document.getElementById('command-text'); 73 34 const resultsContainer = document.getElementById('results'); 74 35 75 - // Set placeholder and focus the input 76 - commandInput.placeholder = 'Start typing...'; 36 + // Set up input tracking 37 + commandInput.value = ''; 77 38 commandInput.focus(); 78 39 79 40 // Add event listeners to the input 80 - commandInput.addEventListener('keyup', onKeyup); 41 + commandInput.addEventListener('input', () => { 42 + state.typed = commandInput.value; 43 + if (state.typed) { 44 + // Special case: if input contains a space, display matches but highlight the prefix 45 + const spaceIndex = state.typed.indexOf(' '); 46 + if (spaceIndex !== -1) { 47 + const prefix = state.typed.substring(0, spaceIndex); 48 + const temp = findMatchingCommands(prefix); 49 + if (temp.length > 0) { 50 + state.matches = temp; 51 + state.matchIndex = 0; 52 + } else { 53 + state.matches = findMatchingCommands(state.typed); 54 + state.matchIndex = 0; 55 + } 56 + } else { 57 + // Regular case: update matches based on typed text 58 + state.matches = findMatchingCommands(state.typed); 59 + state.matchIndex = 0; 60 + } 61 + } else { 62 + state.matches = []; 63 + state.matchIndex = 0; 64 + } 65 + updateCommandUI(); 66 + updateResultsUI(); 67 + }); 68 + 81 69 commandInput.addEventListener('keydown', (e) => { 82 - // Allow arrows, tab, escape 83 - if (!['ArrowUp', 'ArrowDown', 'Tab', 'Escape', 'Enter'].includes(e.key)) { 84 - return; // Don't prevent default for normal typing 70 + if (['ArrowUp', 'ArrowDown', 'Tab', 'Escape', 'Enter'].includes(e.key)) { 71 + e.preventDefault(); // Prevent default for special keys 72 + handleSpecialKey(e); 85 73 } 86 - e.preventDefault(); // Prevent default for special keys 87 74 }); 88 75 89 - // Make sure the input stays focused 76 + // Keep focus on input 90 77 window.addEventListener('blur', () => { 91 78 setTimeout(() => commandInput.focus(), 10); 92 79 }); ··· 95 82 commandInput.focus(); 96 83 }); 97 84 98 - // Automatically focus the input when the window loads and position cursor at end 85 + // Handle visibility changes 86 + document.addEventListener('visibilitychange', () => { 87 + if (!document.hidden) { 88 + updateCommandUI(); 89 + } 90 + }); 91 + 92 + // Initial focus 99 93 setTimeout(() => { 100 94 commandInput.focus(); 101 - // Place cursor at the end of the input 102 - const length = commandInput.value.length; 103 - commandInput.setSelectionRange(length, length); 104 95 }, 50); 105 96 } 106 97 107 98 render(); 108 99 109 - async function css(el, props) { 110 - Object.keys(props).forEach(p => el.style[p] = props[p]); 100 + /** 101 + * Handles special key presses (arrows, tab, enter, escape) 102 + */ 103 + function handleSpecialKey(e) { 104 + const commandInput = document.getElementById('command-input'); 105 + 106 + // Escape key - close window 107 + if (e.key === 'Escape' && !hasModifier(e)) { 108 + shutdown(); 109 + return; 110 + } 111 + 112 + // Enter key - execute command 113 + if (e.key === 'Enter' && !hasModifier(e)) { 114 + const name = state.matches[state.matchIndex]; 115 + if (name && Object.keys(state.commands).indexOf(name) > -1) { 116 + // Preserve any parameters when executing 117 + const typedText = commandInput.value; 118 + 119 + // Store command name for history and feedback 120 + const commandPart = typedText.split(' ')[0]; 121 + state.lastExecuted = name; 122 + updateMatchCount(name); 123 + updateMatchFeedback(commandPart, name); 124 + 125 + // Execute with full typed text 126 + execute(name, typedText); 127 + 128 + // Clear input and UI 129 + commandInput.value = ''; 130 + state.typed = ''; 131 + updateCommandUI(); 132 + updateResultsUI(); 133 + } 134 + return; 135 + } 136 + 137 + // Arrow Up - navigate results up 138 + if (e.key === 'ArrowUp' && state.matchIndex > 0) { 139 + state.matchIndex--; 140 + updateCommandUI(); 141 + updateResultsUI(); 142 + return; 143 + } 144 + 145 + // Arrow Down - navigate results down 146 + if (e.key === 'ArrowDown' && state.matchIndex + 1 < state.matches.length) { 147 + state.matchIndex++; 148 + updateCommandUI(); 149 + updateResultsUI(); 150 + return; 151 + } 152 + 153 + // Tab key - autocomplete 154 + if (e.key === 'Tab' && state.matches.length > 0) { 155 + // Get any parameters after the command (text after a space) 156 + const params = commandInput.value.includes(' ') 157 + ? commandInput.value.substring(commandInput.value.indexOf(' ')) 158 + : ''; 159 + 160 + // Set the command to the full match plus any parameters 161 + state.typed = state.matches[state.matchIndex] + params; 162 + commandInput.value = state.typed; 163 + 164 + // Update UI and matches 165 + state.matches = findMatchingCommands(state.typed); 166 + updateCommandUI(); 167 + updateResultsUI(); 168 + 169 + // Place cursor at the end 170 + setTimeout(() => { 171 + commandInput.setSelectionRange(commandInput.value.length, commandInput.value.length); 172 + }, 0); 173 + 174 + return; 175 + } 111 176 } 112 177 178 + /** 179 + * Executes a command 180 + */ 113 181 async function execute(name, typed) { 114 182 if (state.commands[name]) { 115 - console.log('executing cmd', name, typed); 116 - 117 - // execute command 183 + debug && console.log('executing cmd', name, typed); 118 184 const msg = state.commands[name].execute({typed}); 119 - 120 - // close cmd popup 121 - // NOTE: this kills command execution 122 - // hrghhh, gotta turn execution completion promise 123 - // or run em async in background script 124 - setTimeout(shutdown, 100) 185 + setTimeout(shutdown, 100); 125 186 } 126 187 } 127 188 189 + /** 190 + * Closes the window 191 + */ 192 + async function shutdown() { 193 + window.close(); 194 + } 195 + 196 + /** 197 + * Finds commands matching the typed text 198 + */ 128 199 function findMatchingCommands(text) { 129 - console.log('findMatchingCommands', text, state.commands.length); 200 + const r = debug; // Only log if in debug mode 201 + r && console.log('findMatchingCommands', text, state.commands.length); 130 202 131 - let count = state.commands.length, 132 - matches = []; 203 + let matches = []; 204 + 205 + // No text, no matches 206 + if (!text) { 207 + return matches; 208 + } 209 + 210 + // Get the command part (text before the first space) 211 + const commandPart = text.split(' ')[0]; 212 + const hasParameters = text.includes(' '); 213 + 214 + r && console.log('Command part:', commandPart, 'Has parameters:', hasParameters); 133 215 134 216 // Iterate over all commands, searching for matches 135 - //for (var i = 0; i < count; i++) { 136 - //for (const [name, properties] of Object.entries(state.commands)) { 137 217 for (const name of Object.keys(state.commands)) { 138 218 // Match when: 139 219 // 1. typed string is anywhere in a command name 140 - // 2. command name is at beginning of typed string 141 - // (eg: for command input - "weather san diego") 142 - console.log('testing option...', name); 143 - if (name.toLowerCase().indexOf(state.typed.toLowerCase()) != -1 || 144 - state.typed.toLowerCase().indexOf(name.toLowerCase()) === 0) { 220 + // 2. command name is at beginning of typed string (for commands with parameters) 221 + r && console.log('testing option...', name); 222 + 223 + const matchesCommand = name.toLowerCase().indexOf(commandPart.toLowerCase()) !== -1; 224 + const isCommandWithParams = hasParameters && text.toLowerCase().startsWith(name.toLowerCase() + ' '); 225 + 226 + if (matchesCommand || isCommandWithParams) { 145 227 matches.push(name); 146 228 } 147 229 } 148 230 149 - // sort by match count 150 - state.matches.sort(function(a, b) { 151 - var aCount = state.matchCounts[a] || 0; 152 - var bCount = state.matchCounts[b] || 0; 231 + // Sort by match count 232 + matches.sort(function(a, b) { 233 + const aCount = state.matchCounts[a] || 0; 234 + const bCount = state.matchCounts[b] || 0; 153 235 return bCount - aCount; 154 - }) 236 + }); 155 237 156 - // insert adaptive feedback 157 - if (state.matchFeedback[state.typed]) { 158 - state.matches.unshift(state.matchFeedback[state.typed]) 238 + // Insert adaptive feedback at the top if present 239 + if (state.matchFeedback[text]) { 240 + // Check if it's already in the list 241 + const feedbackIndex = matches.indexOf(state.matchFeedback[text]); 242 + if (feedbackIndex !== -1) { 243 + // Move to the beginning 244 + matches.splice(feedbackIndex, 1); 245 + } 246 + matches.unshift(state.matchFeedback[text]); 159 247 } 160 248 161 249 return matches; 162 250 } 163 251 252 + /** 253 + * Updates the match feedback for adaptive suggestions 254 + */ 164 255 function updateMatchFeedback(typed, name) { 165 256 state.matchFeedback[typed] = name; 166 257 } 167 258 259 + /** 260 + * Updates the match count for frequency sorting 261 + */ 168 262 function updateMatchCount(name) { 169 - if (!state.matchCounts[name]); 263 + if (!state.matchCounts[name]) { 170 264 state.matchCounts[name] = 0; 171 - state.matchCounts[name]++; 172 - } 173 - 174 - async function shutdown() { 175 - window.close(); 176 - /* 177 - let container = document.querySelector('#cmdContainer'); 178 - if (container) { 179 - document.body.removeChild(container); 180 265 } 181 - document.removeEventListener('keyup', onKeyup, true); 182 - document.removeEventListener('keypress', onKeyDummyStop, true); 183 - document.removeEventListener('keydown', onKeyDummyStop, true); 184 - document.removeEventListener('input', onKeyDummyStop, true); 185 - */ 186 - //console.log('ui shutdown complete'); 266 + state.matchCounts[name]++; 187 267 } 188 268 189 - function onKeyDummyStop(e) { 190 - e.preventDefault(); 191 - } 192 - 193 - async function onKeyup(e) { 194 - // Get the command input element and results container 195 - const commandInput = document.getElementById('command-input'); 196 - const resultsContainer = document.getElementById('results'); 269 + /** 270 + * Updates the command text UI with proper highlighting 271 + */ 272 + function updateCommandUI() { 273 + const commandText = document.getElementById('command-text'); 274 + commandText.innerHTML = ''; 197 275 198 - // Use the input value as the typed text 199 - state.typed = commandInput.value; 200 - 201 - console.log('onKeyup', e.key, state.typed); 202 - 203 - if (isModifier(e)) { 276 + // If no matches or no typed text, clear the suggestion 277 + if (state.matches.length === 0 || !state.typed) { 204 278 return; 205 279 } 206 - 207 - // if user pressed escape, go away 208 - if (e.key == 'Escape' && !hasModifier(e)) { 209 - await shutdown(); 280 + 281 + const selectedMatch = state.matches[state.matchIndex]; 282 + if (!selectedMatch) { 210 283 return; 211 284 } 212 - 213 - // if user pressed return, attempt to execute command 214 - if (e.key == 'Enter' && !hasModifier(e)) { 215 - console.log('enter pressed', state.typed); 216 - let name = state.matches[state.matchIndex]; 217 - if (name && Object.keys(state.commands).indexOf(name) > -1) { 218 - execute(name, state.typed); 219 - state.lastExecuted = name; 220 - updateMatchCount(name); 221 - updateMatchFeedback(state.typed, name); 222 - commandInput.value = ''; 223 - state.typed = ''; 224 - resultsContainer.innerHTML = ''; 225 - } 226 - return; 285 + 286 + // Check if we have parameters (text after space) 287 + const hasParameters = state.typed.includes(' '); 288 + let matchText = state.typed; 289 + 290 + // If we have parameters, only match against the command part 291 + if (hasParameters) { 292 + matchText = state.typed.substring(0, state.typed.indexOf(' ')); 227 293 } 228 - 229 - // Handle up/down arrows for navigation 230 - if (e.key == 'ArrowUp' && state.matchIndex > 0) { 231 - state.matchIndex--; 232 - updateResultsUI(); 233 - return; 294 + 295 + // Find the matching part in the selected command 296 + const lowerSelected = selectedMatch.toLowerCase(); 297 + const lowerMatchText = matchText.toLowerCase(); 298 + 299 + // Calculate match information 300 + let matchIndex = lowerSelected.indexOf(lowerMatchText); 301 + 302 + // Special case for prefix match 303 + if (matchIndex === -1 && lowerMatchText && lowerSelected.startsWith(lowerMatchText)) { 304 + matchIndex = 0; 234 305 } 235 - 236 - if (e.key == 'ArrowDown' && state.matchIndex + 1 < state.matches.length) { 237 - state.matchIndex++; 238 - updateResultsUI(); 239 - return; 240 - } 241 - 242 - // Handle tab for autocompletion 243 - if (e.key == 'Tab' && state.matches && state.matches.length > 0) { 244 - commandInput.value = state.matches[state.matchIndex]; 245 - state.typed = state.matches[state.matchIndex]; 246 - return; 306 + 307 + // If we found a match in the command part 308 + if (matchIndex !== -1) { 309 + // Split the command into parts 310 + const beforeMatch = selectedMatch.substring(0, matchIndex); 311 + const matchPart = selectedMatch.substring(matchIndex, matchIndex + matchText.length); 312 + const afterMatch = selectedMatch.substring(matchIndex + matchText.length); 313 + 314 + // Before the match 315 + if (beforeMatch) { 316 + const beforeSpan = document.createElement('span'); 317 + beforeSpan.textContent = beforeMatch; 318 + commandText.appendChild(beforeSpan); 319 + } 320 + 321 + // The matched part (underlined) 322 + const matchSpan = document.createElement('span'); 323 + matchSpan.className = 'matched'; 324 + matchSpan.textContent = matchPart; 325 + commandText.appendChild(matchSpan); 326 + 327 + // After the match 328 + if (afterMatch) { 329 + const afterSpan = document.createElement('span'); 330 + afterSpan.textContent = afterMatch; 331 + commandText.appendChild(afterSpan); 332 + } 333 + 334 + // Add parameters if present 335 + if (hasParameters) { 336 + const paramsText = state.typed.substring(state.typed.indexOf(' ')); 337 + const paramsSpan = document.createElement('span'); 338 + paramsSpan.textContent = paramsText; 339 + commandText.appendChild(paramsSpan); 340 + } 341 + } else { 342 + // No match found - just clear the suggestion 343 + commandText.textContent = ''; 247 344 } 248 - 249 - // Update matches based on typed text 250 - state.matches = findMatchingCommands(state.typed); 251 - state.matchIndex = 0; 252 - 253 - // Update the results UI 254 - updateResultsUI(); 255 345 } 256 346 347 + /** 348 + * Updates the results list UI 349 + */ 257 350 function updateResultsUI() { 258 351 const resultsContainer = document.getElementById('results'); 259 352 resultsContainer.innerHTML = ''; ··· 280 373 updateMatchFeedback(state.typed, match); 281 374 document.getElementById('command-input').value = ''; 282 375 state.typed = ''; 283 - resultsContainer.innerHTML = ''; 376 + updateCommandUI(); 377 + updateResultsUI(); 284 378 }); 285 379 286 380 resultsContainer.appendChild(item); 287 381 }); 288 382 } 289 383 384 + /** 385 + * Checks if an event has modifier keys 386 + */ 290 387 function hasModifier(e) { 291 388 return e.altKey || e.ctrlKey || e.metaKey; 292 389 } 293 390 391 + /** 392 + * Checks if a key is a modifier key 393 + */ 294 394 function isModifier(e) { 295 - return ['Alt', 'Control', 'Shift', 'Meta'].indexOf(e.key) != -1; 296 - } 297 - 298 - function isIgnorable(e) { 299 - switch(e.which) { 300 - case 38: //up arrow 301 - case 40: //down arrow 302 - case 37: //left arrow 303 - case 39: //right arrow 304 - case 33: //page up 305 - case 34: //page down 306 - case 36: //home 307 - case 35: //end 308 - case 13: //enter 309 - case 9: //tab 310 - case 27: //esc 311 - case 16: //shift 312 - case 17: //ctrl 313 - case 18: //alt 314 - case 20: //caps lock 315 - // we handle this for editing 316 - //case 8: //backspace 317 - // need to handle for editing also? 318 - case 46: //delete 319 - case 224: //meta 320 - case 0: 321 - return true; 322 - break; 323 - default: 324 - return false; 325 - } 326 - } 327 - 328 - // These functions are replaced by the new updateResultsUI function that 329 - // works with the actual HTML input field instead of custom rendering 395 + return ['Alt', 'Control', 'Shift', 'Meta'].indexOf(e.key) !== -1; 396 + }