Rewild Your Web
web
browser
dweb
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();