Rewild Your Web
web browser dweb
at main 186 lines 5.5 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3import { SearchController } from "//shared.localhost:8888/search/controller.js"; 4 5const container = document.getElementById("container"); 6const searchInput = document.getElementById("search-input"); 7const resultsList = document.getElementById("results-list"); 8 9// BroadcastChannel for communicating with main browser window 10const searchChannel = new BroadcastChannel("servo-search"); 11let discoveredWindows = []; 12let pendingNavigation = null; 13 14// Listen for responses from browser windows 15searchChannel.onmessage = (e) => { 16 if (e.data.type === "available") { 17 // Collect discovered browser windows 18 discoveredWindows.push(e.data.windowId); 19 } else if ( 20 e.data.type === "ack" && 21 pendingNavigation && 22 e.data.id === pendingNavigation.id 23 ) { 24 // Browser window handled it, close search window 25 clearTimeout(pendingNavigation.timeout); 26 pendingNavigation = null; 27 // navigator.embedder.closeCurrentOSWindow(); 28 } 29}; 30 31// Navigate to a URL - open in existing browser window or create new one 32function navigateTo(url) { 33 discoveredWindows = []; 34 35 // Phase 1: Discover available browser windows 36 searchChannel.postMessage({ type: "discover" }); 37 38 // Phase 2: After brief discovery period, send to first responder or open new window 39 setTimeout(() => { 40 if (discoveredWindows.length > 0) { 41 // Send to first discovered window 42 const targetWindowId = discoveredWindows[0]; 43 const id = Date.now(); 44 45 searchChannel.postMessage({ 46 type: "openUrl", 47 url: url, 48 id: id, 49 targetWindowId: targetWindowId, 50 }); 51 52 // Set up fallback timeout 53 pendingNavigation = { 54 id: id, 55 timeout: setTimeout(() => { 56 // Target window didn't ack - open new window as fallback 57 pendingNavigation = null; 58 navigator.embedder.openNewOSWindow( 59 `//system.localhost:8888/index.html?open=${encodeURIComponent(url)}`, 60 ); 61 // navigator.embedder.closeCurrentOSWindow(); 62 }, 100), 63 }; 64 } else { 65 // No browser windows found - open new window 66 navigator.embedder.openNewOSWindow( 67 `//system.localhost:8888/index.html?open=${encodeURIComponent(url)}`, 68 ); 69 // navigator.embedder.closeCurrentOSWindow(); 70 } 71 }, 50); // 50ms discovery window 72} 73 74// Select a web-view in an existing browser window 75function selectWebView(windowId, webviewId) { 76 const id = Date.now(); 77 78 // Send selectWebView message to the specific window 79 searchChannel.postMessage({ 80 type: "selectWebView", 81 id: id, 82 targetWindowId: windowId, 83 webviewId: webviewId, 84 }); 85 86 // Set up fallback timeout (in case window closed) 87 pendingNavigation = { 88 id: id, 89 timeout: setTimeout(() => { 90 pendingNavigation = null; 91 // Window didn't respond - just close search 92 // navigator.embedder.closeCurrentOSWindow(); 93 }, 100), 94 }; 95} 96 97// Initialize search controller 98const controller = new SearchController({ 99 onNavigate: navigateTo, 100 onSelectWebView: selectWebView, 101 onResultsChanged: renderResults, 102}); 103 104// Render results to the DOM 105function renderResults(results, groups) { 106 resultsList.innerHTML = ""; 107 108 if (results.length === 0) { 109 container.classList.remove("has-results"); 110 return; 111 } 112 113 container.classList.add("has-results"); 114 115 for (const group of groups) { 116 const groupDiv = document.createElement("div"); 117 groupDiv.className = "result-group"; 118 119 // Icon column 120 const iconDiv = document.createElement("div"); 121 iconDiv.className = "result-group-icon"; 122 if (group.providerIcon) { 123 const icon = document.createElement("lucide-icon"); 124 icon.setAttribute("name", group.providerIcon); 125 iconDiv.appendChild(icon); 126 } 127 groupDiv.appendChild(iconDiv); 128 129 // Items column 130 const itemsDiv = document.createElement("div"); 131 itemsDiv.className = "result-group-items"; 132 133 for (const result of group.items) { 134 const itemDiv = document.createElement("div"); 135 itemDiv.className = "result-item"; 136 itemDiv.dataset.kind = result.kind; 137 138 if (result.kind === "link" || result.kind === "webview") { 139 const link = document.createElement("a"); 140 link.href = result.kind === "link" ? result.value.url : "#"; 141 link.className = "result-link"; 142 link.textContent = result.value.title; 143 link.addEventListener("click", (e) => { 144 e.preventDefault(); 145 controller.handleResultClick(result); 146 }); 147 itemDiv.appendChild(link); 148 } else if (result.kind === "text") { 149 const text = document.createElement("span"); 150 text.className = "result-text"; 151 text.textContent = result.value; 152 itemDiv.appendChild(text); 153 } 154 155 itemsDiv.appendChild(itemDiv); 156 } 157 158 groupDiv.appendChild(itemsDiv); 159 resultsList.appendChild(groupDiv); 160 } 161} 162 163// Event listeners 164searchInput.addEventListener("input", () => { 165 controller.query(searchInput.value); 166}); 167 168searchInput.addEventListener("keydown", (e) => { 169 if (e.key === "Enter") { 170 e.preventDefault(); 171 controller.handleSubmit(searchInput.value); 172 } 173}); 174 175// Close window on Escape from anywhere 176document.addEventListener("keydown", (e) => { 177 if (e.key === "Escape") { 178 navigator.embedder.closeCurrentOSWindow(); 179 } 180}); 181 182document.querySelector("h1").addEventListener("mousedown", () => { 183 navigator.embedder.startWindowDrag(); 184}); 185 186searchInput.focus();