experiments in a post-browser web
10
fork

Configure Feed

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

feat(hud): migrate HUD to widget sheet system with individual widget pages

+621 -327
+82 -2
extensions/hud/background.js
··· 2 2 * HUD Extension Background Script 3 3 * 4 4 * Always-on-top overlay showing current mode, IZUI state, and window context. 5 + * Uses the widget sheet system — each piece of HUD info is an individual 6 + * widget page rendered via webview in a peek-grid freeform layout. 7 + * 5 8 * Runs in isolated extension process (peek://ext/hud/background.html) 6 9 */ 7 10 ··· 12 15 13 16 const HUD_ADDRESS = 'peek://ext/hud/hud.html'; 14 17 const STORAGE_KEY = 'hud_enabled'; 18 + const HUD_SHEET_KEY = 'hud_sheet'; 19 + 20 + // Default HUD sheet layout — arranges widget pages vertically 21 + const WIDGET_WIDTH = 220; 22 + const WIDGET_HEIGHT = 40; 23 + const WIDGET_GAP = 2; 24 + 25 + const DEFAULT_HUD_WIDGETS = [ 26 + { id: 'mode', url: 'peek://ext/hud/widgets/mode.html' }, 27 + { id: 'izui', url: 'peek://ext/hud/widgets/izui.html' }, 28 + { id: 'window', url: 'peek://ext/hud/widgets/window.html' }, 29 + { id: 'stats', url: 'peek://ext/hud/widgets/stats.html' } 30 + ]; 31 + 32 + /** 33 + * Build the default HUD sheet config 34 + * Arranges widgets in a vertical stack 35 + */ 36 + const buildDefaultSheetConfig = () => { 37 + const items = DEFAULT_HUD_WIDGETS.map((widget, index) => ({ 38 + id: widget.id, 39 + url: widget.url, 40 + x: 0, 41 + y: index * (WIDGET_HEIGHT + WIDGET_GAP), 42 + width: WIDGET_WIDTH, 43 + height: WIDGET_HEIGHT 44 + })); 45 + 46 + return { 47 + version: 1, 48 + name: 'HUD', 49 + createdAt: Date.now(), 50 + items 51 + }; 52 + }; 53 + 54 + /** 55 + * Ensure the HUD sheet config exists in extension_settings. 56 + * Creates a default config if none exists. 57 + */ 58 + const ensureSheetConfig = async () => { 59 + const result = await api.settings.getKey(HUD_SHEET_KEY); 60 + if (result.success && result.data) { 61 + debug && console.log('[ext:hud] HUD sheet config already exists'); 62 + return result.data; 63 + } 64 + 65 + // Create default config 66 + const config = buildDefaultSheetConfig(); 67 + await api.settings.setKey(HUD_SHEET_KEY, config); 68 + console.log('[ext:hud] Created default HUD sheet config with', config.items.length, 'widgets'); 69 + return config; 70 + }; 15 71 16 72 // Track HUD state 17 73 let hudEnabled = false; ··· 71 127 }; 72 128 73 129 /** 130 + * Calculate HUD window size from sheet config 131 + */ 132 + const getHudWindowSize = (config) => { 133 + let maxRight = 0; 134 + let maxBottom = 0; 135 + for (const item of config.items) { 136 + maxRight = Math.max(maxRight, item.x + item.width); 137 + maxBottom = Math.max(maxBottom, item.y + item.height); 138 + } 139 + // Add container padding (10px each side) + border 140 + return { 141 + width: maxRight + 22, 142 + height: maxBottom + 22 143 + }; 144 + }; 145 + 146 + /** 74 147 * Open the HUD window 75 148 */ 76 149 const openHud = async () => { ··· 85 158 hudWindowId = null; 86 159 } 87 160 161 + // Ensure sheet config exists before opening 162 + const config = await ensureSheetConfig(); 163 + const size = getHudWindowSize(config); 164 + 88 165 const params = { 89 166 key: HUD_ADDRESS, 90 - width: 240, 91 - height: 300, 167 + width: size.width, 168 + height: size.height, 92 169 x: 20, 93 170 y: 20, 94 171 transparent: true, ··· 223 300 224 301 // Load persisted state 225 302 await loadState(); 303 + 304 + // Ensure HUD sheet config exists (create default if needed) 305 + await ensureSheetConfig(); 226 306 227 307 // Register shortcut 228 308 initShortcut();
+23 -24
extensions/hud/hud.html
··· 6 6 <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"> 7 7 <title>HUD</title> 8 8 <link rel="stylesheet" type="text/css" href="styles.css"> 9 + 10 + <!-- Import map for resolving bare module specifiers --> 11 + <script type="importmap"> 12 + { 13 + "imports": { 14 + "lit": "peek://node_modules/lit/index.js", 15 + "lit/": "peek://node_modules/lit/", 16 + "lit-html": "peek://node_modules/lit-html/lit-html.js", 17 + "lit-html/": "peek://node_modules/lit-html/", 18 + "lit-element": "peek://node_modules/lit-element/lit-element.js", 19 + "lit-element/": "peek://node_modules/lit-element/", 20 + "@lit/reactive-element": "peek://node_modules/@lit/reactive-element/reactive-element.js", 21 + "@lit/reactive-element/": "peek://node_modules/@lit/reactive-element/" 22 + } 23 + } 24 + </script> 25 + 26 + <!-- Import peek-components --> 27 + <script type="module"> 28 + import 'peek://app/components/peek-card.js'; 29 + import 'peek://app/components/peek-grid.js'; 30 + </script> 9 31 </head> 10 32 <body> 11 33 <div id="hud-container"> 12 - <div id="group-accent" class="group-accent" style="display: none;"></div> 13 - <div id="group-banner" class="group-banner" style="display: none;"> 14 - <div id="group-name" class="group-name"></div> 15 - </div> 16 - 17 - <div class="hud-section"> 18 - <div class="hud-label">Mode</div> 19 - <div id="mode-value" class="hud-value">-</div> 20 - </div> 21 - 22 - <div class="hud-section"> 23 - <div class="hud-label">IZUI State</div> 24 - <div id="izui-value" class="hud-value">-</div> 25 - </div> 26 - 27 - <div class="hud-section"> 28 - <div class="hud-label">Active Window</div> 29 - <div id="window-title" class="hud-value hud-truncate">-</div> 30 - </div> 31 - 32 - <div class="hud-section"> 33 - <div class="hud-label">Stats</div> 34 - <div id="stats-value" class="hud-value hud-small">-</div> 35 - </div> 34 + <peek-grid class="hud-grid" view-mode="freeform" freeform-snap="0" gap="0"></peek-grid> 36 35 </div> 37 36 38 37 <script type="module" src="hud.js"></script>
+86 -224
extensions/hud/hud.js
··· 1 1 /** 2 2 * HUD Display Script 3 3 * 4 - * Reactive display showing current mode, IZUI state, and window context. 4 + * Renders HUD widget pages in a peek-grid freeform layout. 5 + * Each widget is a webview pointing to an individual widget page 6 + * (e.g., peek://ext/hud/widgets/mode.html). 7 + * 8 + * The layout config is loaded from extension_settings (created by background.js). 5 9 */ 6 10 7 11 const api = window.app; 8 12 const debug = api.debug; 9 13 10 - console.log('[hud] Display script loaded'); 11 - 12 - // DOM elements 13 - const modeValue = document.getElementById('mode-value'); 14 - const izuiValue = document.getElementById('izui-value'); 15 - const windowTitle = document.getElementById('window-title'); 16 - const statsValue = document.getElementById('stats-value'); 17 - const groupAccent = document.getElementById('group-accent'); 18 - const groupBanner = document.getElementById('group-banner'); 19 - const groupName = document.getElementById('group-name'); 20 - const hudContainer = document.getElementById('hud-container'); 14 + const HUD_SHEET_KEY = 'hud_sheet'; 21 15 22 - // Current state 23 - let currentMode = 'default'; 24 - let currentModeMetadata = {}; 25 - let currentIzuiState = 'idle'; 26 - let currentWindowInfo = null; 27 - let windowCount = 0; 28 - 29 - // Auto-resize window to fit content 30 - let resizeTimer = null; 31 - const autoResizeWindow = () => { 32 - // Debounce to avoid excessive resize calls 33 - if (resizeTimer) clearTimeout(resizeTimer); 34 - resizeTimer = setTimeout(() => { 35 - const container = document.getElementById('hud-container'); 36 - if (!container) return; 37 - // Measure the actual rendered height of the container content 38 - const contentHeight = container.scrollHeight + 2; // +2 for border 39 - const contentWidth = 240; 40 - api.window.resize(contentWidth, contentHeight); 41 - debug && console.log('[hud] Auto-resized window to:', contentWidth, contentHeight); 42 - }, 50); 43 - }; 16 + let sheetConfig = null; 44 17 45 18 /** 46 - * Update mode display 19 + * Load HUD sheet config from extension_settings 47 20 */ 48 - const updateModeDisplay = () => { 49 - if (!modeValue) return; 50 - 51 - // Remove old mode classes 52 - modeValue.classList.remove('mode-default', 'mode-page', 'mode-group', 'mode-settings'); 53 - 54 - // Format mode display 55 - let displayText = currentMode; 56 - if (currentMode === 'group' && currentModeMetadata.groupName) { 57 - displayText = `group: ${currentModeMetadata.groupName}`; 21 + const loadSheetConfig = async () => { 22 + const result = await api.settings.getKey(HUD_SHEET_KEY); 23 + if (result.success && result.data) { 24 + return result.data; 58 25 } 59 - 60 - modeValue.textContent = displayText; 61 - modeValue.classList.add(`mode-${currentMode}`); 62 - 63 - // Update group visual treatment 64 - if (currentMode === 'group' && currentModeMetadata.groupName) { 65 - const color = currentModeMetadata.color || '#999'; 66 - groupAccent.style.background = color; 67 - groupAccent.style.display = ''; 68 - groupName.textContent = currentModeMetadata.groupName; 69 - groupBanner.style.display = ''; 70 - hudContainer.classList.add('hud-group-active'); 71 - hudContainer.style.setProperty('--group-color', color + '40'); 72 - } else { 73 - groupAccent.style.display = 'none'; 74 - groupBanner.style.display = 'none'; 75 - hudContainer.classList.remove('hud-group-active'); 76 - hudContainer.style.removeProperty('--group-color'); 77 - } 78 - 79 - autoResizeWindow(); 80 - debug && console.log('[hud] Updated mode display:', displayText); 26 + return null; 81 27 }; 82 28 83 29 /** 84 - * Update IZUI state display 30 + * Create a peek-card with a webview for a widget item 85 31 */ 86 - const updateIzuiDisplay = () => { 87 - if (!izuiValue) return; 32 + const createWidgetCard = (item) => { 33 + const card = document.createElement('peek-card'); 34 + card.id = item.id; 35 + card.dataset.id = item.id; 88 36 89 - // Remove old state classes 90 - izuiValue.classList.remove('izui-idle', 'izui-transient', 'izui-active', 'izui-overlay'); 37 + // Webview hosting the widget page 38 + const webview = document.createElement('webview'); 39 + webview.className = 'widget-webview'; 40 + webview.src = item.url; 41 + webview.setAttribute('nodeintegration', ''); 42 + card.appendChild(webview); 91 43 92 - izuiValue.textContent = currentIzuiState; 93 - izuiValue.classList.add(`izui-${currentIzuiState}`); 94 - 95 - debug && console.log('[hud] Updated IZUI display:', currentIzuiState); 44 + return card; 96 45 }; 97 46 98 47 /** 99 - * Update active window display 48 + * Render all widget cards from config 100 49 */ 101 - const updateWindowDisplay = () => { 102 - if (!windowTitle) return; 50 + const renderWidgets = () => { 51 + const grid = document.querySelector('peek-grid.hud-grid'); 52 + if (!grid) return; 103 53 104 - if (currentWindowInfo && currentWindowInfo.title) { 105 - let displayText = currentWindowInfo.title; 54 + grid.innerHTML = ''; 106 55 107 - // Add URL if available (for web pages) 108 - if (currentWindowInfo.url && 109 - !currentWindowInfo.url.startsWith('peek://') && 110 - currentWindowInfo.url !== currentWindowInfo.title) { 111 - displayText = currentWindowInfo.title; 112 - } 56 + if (!sheetConfig || !sheetConfig.items || sheetConfig.items.length === 0) { 57 + console.warn('[hud] No widgets in sheet config'); 58 + return; 59 + } 113 60 114 - windowTitle.textContent = displayText; 115 - } else { 116 - windowTitle.textContent = 'No active window'; 61 + // Build freeform layout map from config 62 + const layout = {}; 63 + for (const item of sheetConfig.items) { 64 + layout[item.id] = { 65 + x: item.x, 66 + y: item.y, 67 + w: item.width, 68 + h: item.height 69 + }; 117 70 } 118 71 119 - debug && console.log('[hud] Updated window display:', currentWindowInfo?.title); 120 - }; 121 - 122 - /** 123 - * Update stats display 124 - */ 125 - const updateStatsDisplay = () => { 126 - if (!statsValue) return; 72 + // Create widget cards 73 + for (const item of sheetConfig.items) { 74 + const card = createWidgetCard(item); 75 + grid.appendChild(card); 76 + } 127 77 128 - const parts = []; 129 - parts.push(`${windowCount} window${windowCount !== 1 ? 's' : ''}`); 78 + // Set freeform layout on grid 79 + grid.freeformLayout = layout; 130 80 131 - statsValue.textContent = parts.join(' • '); 81 + // Auto-resize window to fit all widgets 132 82 autoResizeWindow(); 133 - 134 - debug && console.log('[hud] Updated stats display:', windowCount); 135 83 }; 136 84 137 85 /** 138 - * Refresh all window info 86 + * Auto-resize the HUD window to fit widget content 139 87 */ 140 - const refreshWindowInfo = async () => { 141 - try { 142 - // Get list of windows and the focused visible window ID 143 - const [windowsResult, focusedWindowId] = await Promise.all([ 144 - api.window.list(), 145 - api.window.getFocusedVisibleWindowId() 146 - ]); 88 + let resizeTimer = null; 89 + const autoResizeWindow = () => { 90 + if (resizeTimer) clearTimeout(resizeTimer); 91 + resizeTimer = setTimeout(() => { 92 + if (!sheetConfig || !sheetConfig.items || sheetConfig.items.length === 0) return; 147 93 148 - if (windowsResult.success && windowsResult.windows) { 149 - windowCount = windowsResult.windows.length; 150 - 151 - // Find the focused visible window in the list 152 - const focusedWindow = focusedWindowId 153 - ? windowsResult.windows.find(w => w.id === focusedWindowId) 154 - : null; 155 - 156 - if (focusedWindow) { 157 - currentWindowInfo = { 158 - title: focusedWindow.title || 'Untitled', 159 - url: focusedWindow.url || focusedWindow.source 160 - }; 161 - } else { 162 - currentWindowInfo = null; 163 - } 164 - 165 - updateWindowDisplay(); 166 - updateStatsDisplay(); 94 + // Calculate bounding box of all widgets 95 + let maxRight = 0; 96 + let maxBottom = 0; 97 + for (const item of sheetConfig.items) { 98 + maxRight = Math.max(maxRight, item.x + item.width); 99 + maxBottom = Math.max(maxBottom, item.y + item.height); 167 100 } 168 - } catch (error) { 169 - console.error('[hud] Error refreshing window info:', error); 170 - } 171 - }; 172 101 173 - /** 174 - * Refresh IZUI state 175 - */ 176 - const refreshIzuiState = async () => { 177 - try { 178 - const state = await api.izui.getState(); 179 - if (state) { 180 - currentIzuiState = state; 181 - updateIzuiDisplay(); 182 - } 183 - } catch (error) { 184 - console.error('[hud] Error getting IZUI state:', error); 185 - } 186 - }; 102 + // Add padding for the container 103 + const width = maxRight + 20; // 10px padding each side 104 + const height = maxBottom + 20; 187 105 188 - /** 189 - * Refresh mode 190 - */ 191 - const refreshMode = async () => { 192 - try { 193 - // Get the focused visible window's mode (not the HUD's own mode) 194 - const focusedWindowId = await api.window.getFocusedVisibleWindowId(); 195 - const result = await api.context.get('mode', focusedWindowId); 196 - if (result.success && result.data) { 197 - currentMode = result.data.value || 'default'; 198 - currentModeMetadata = result.data.metadata || {}; 199 - } else { 200 - // No focused window or no mode set — show "default" 201 - currentMode = 'default'; 202 - currentModeMetadata = {}; 203 - } 204 - updateModeDisplay(); 205 - } catch (error) { 206 - console.error('[hud] Error getting mode:', error); 207 - } 208 - }; 209 - 210 - /** 211 - * Refresh all data 212 - */ 213 - const refreshAll = async () => { 214 - await Promise.all([ 215 - refreshMode(), 216 - refreshIzuiState(), 217 - refreshWindowInfo() 218 - ]); 106 + api.window.resize(width, height); 107 + debug && console.log('[hud] Auto-resized window to:', width, height); 108 + }, 50); 219 109 }; 220 110 221 111 /** 222 112 * Initialize HUD 223 113 */ 224 114 const init = async () => { 225 - console.log('[hud] Initializing display'); 115 + console.log('[hud] Initializing widget sheet display'); 226 116 227 - // Initial data fetch 228 - await refreshAll(); 117 + sheetConfig = await loadSheetConfig(); 229 118 230 - // Auto-resize window to fit initial content 231 - autoResizeWindow(); 119 + if (!sheetConfig) { 120 + console.warn('[hud] No HUD sheet config found — waiting for background.js to create it'); 121 + // Retry after a short delay (background.js may still be initializing) 122 + setTimeout(async () => { 123 + sheetConfig = await loadSheetConfig(); 124 + if (sheetConfig) { 125 + renderWidgets(); 126 + } else { 127 + console.error('[hud] HUD sheet config still not found'); 128 + } 129 + }, 1000); 130 + return; 131 + } 232 132 233 - // Watch for mode changes 234 - api.context.watchMode((mode, entry) => { 235 - if (mode) { 236 - currentMode = mode; 237 - currentModeMetadata = entry?.metadata || {}; 238 - updateModeDisplay(); 239 - } 240 - }); 241 - 242 - // Subscribe to IZUI state changes 243 - api.subscribe('izui:state-changed', (msg) => { 244 - if (msg.state) { 245 - currentIzuiState = msg.state; 246 - updateIzuiDisplay(); 247 - } 248 - }, api.scopes.GLOBAL); 249 - 250 - // Subscribe to window focus changes — refresh mode + window info 251 - api.subscribe('window:focused', () => { 252 - refreshMode(); 253 - refreshWindowInfo(); 254 - }, api.scopes.GLOBAL); 255 - 256 - // Subscribe to window open/close events 257 - api.subscribe('window:opened', () => { 258 - refreshMode(); 259 - refreshWindowInfo(); 260 - }, api.scopes.GLOBAL); 261 - 262 - api.subscribe('window:closed', () => { 263 - refreshMode(); 264 - refreshWindowInfo(); 265 - }, api.scopes.GLOBAL); 266 - 267 - // Periodic refresh (fallback for events we might miss) 268 - setInterval(() => { 269 - refreshAll(); 270 - }, 5000); 271 - 272 - console.log('[hud] Display initialized'); 133 + renderWidgets(); 134 + console.log('[hud] Widget sheet display initialized with', sheetConfig.items.length, 'widgets'); 273 135 }; 274 136 275 137 // Start initialization
+19 -77
extensions/hud/styles.css
··· 30 30 -webkit-backdrop-filter: blur(12px); 31 31 border-radius: 8px; 32 32 border: 1px solid rgba(255, 255, 255, 0.04); 33 - color: rgba(255, 255, 255, 0.45); 34 - font-size: 11px; 35 33 -webkit-app-region: no-drag; 36 34 } 37 35 38 - .hud-section { 39 - margin-bottom: 6px; 40 - padding-bottom: 6px; 41 - border-bottom: 1px solid rgba(255, 255, 255, 0.04); 42 - } 43 - 44 - .hud-section:last-child { 45 - margin-bottom: 0; 46 - padding-bottom: 0; 47 - border-bottom: none; 48 - } 49 - 50 - .hud-label { 51 - font-size: 9px; 52 - font-weight: 500; 53 - text-transform: uppercase; 54 - letter-spacing: 0.5px; 55 - color: rgba(255, 255, 255, 0.25); 56 - margin-bottom: 2px; 57 - } 58 - 59 - .hud-value { 60 - font-size: 12px; 61 - font-weight: 400; 62 - color: rgba(255, 255, 255, 0.45); 63 - line-height: 1.4; 64 - } 65 - 66 - .hud-truncate { 67 - white-space: nowrap; 68 - overflow: hidden; 69 - text-overflow: ellipsis; 70 - max-width: 100%; 71 - } 72 - 73 - .hud-small { 74 - font-size: 11px; 75 - line-height: 1.5; 76 - } 77 - 78 - /* Mode and IZUI state classes (no color overrides — all muted) */ 79 - .mode-default, .mode-page, .mode-settings, 80 - .izui-idle, .izui-transient, .izui-active, .izui-overlay { 81 - color: inherit; 82 - } 83 - 84 - /* Group mode — elevated visual treatment */ 85 - .mode-group { 86 - color: rgba(255, 255, 255, 0.7); 87 - font-weight: 500; 88 - } 89 - 90 - /* Group accent stripe — colored bar at top of HUD */ 91 - .group-accent { 92 - height: 3px; 93 - border-radius: 3px 3px 0 0; 94 - margin: -10px -10px 8px -10px; 95 - background: #999; 96 - } 97 - 98 - /* Group banner — prominent group name */ 99 - .group-banner { 100 - margin-bottom: 8px; 101 - padding-bottom: 6px; 102 - border-bottom: 1px solid rgba(255, 255, 255, 0.06); 36 + /* Widget grid — no padding, the container handles it */ 37 + .hud-grid { 38 + width: 100%; 103 39 } 104 40 105 - .group-name { 106 - font-size: 13px; 107 - font-weight: 600; 108 - color: rgba(255, 255, 255, 0.75); 109 - letter-spacing: 0.2px; 110 - white-space: nowrap; 41 + /* Widget cards — transparent, no chrome */ 42 + peek-card { 43 + --peek-card-bg: transparent; 44 + --peek-card-hover-bg: transparent; 45 + --peek-card-border: transparent; 46 + --peek-card-radius: 0; 47 + --peek-card-padding: 0; 48 + --peek-card-gap: 0; 111 49 overflow: hidden; 112 - text-overflow: ellipsis; 50 + border: none; 51 + box-shadow: none; 113 52 } 114 53 115 - /* When in group mode, tint the container border */ 116 - #hud-container.hud-group-active { 117 - border-color: var(--group-color, rgba(255, 255, 255, 0.04)); 54 + /* Webview inside widget card */ 55 + .widget-webview { 56 + width: 100%; 57 + height: 100%; 58 + border: none; 59 + background: transparent; 118 60 }
+18
extensions/hud/widgets/izui.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"> 6 + <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"> 7 + <title>IZUI State</title> 8 + <link rel="stylesheet" type="text/css" href="widget.css"> 9 + </head> 10 + <body> 11 + <div class="widget"> 12 + <div class="widget-label">IZUI State</div> 13 + <div id="izui-value" class="widget-value">-</div> 14 + </div> 15 + 16 + <script type="module" src="izui.js"></script> 17 + </body> 18 + </html>
+51
extensions/hud/widgets/izui.js
··· 1 + /** 2 + * IZUI State Widget — displays current IZUI state 3 + */ 4 + 5 + const api = window.app; 6 + const debug = api.debug; 7 + 8 + const izuiValue = document.getElementById('izui-value'); 9 + 10 + let currentIzuiState = 'idle'; 11 + 12 + const updateDisplay = () => { 13 + if (!izuiValue) return; 14 + 15 + // Remove old state classes 16 + izuiValue.classList.remove('izui-idle', 'izui-transient', 'izui-active', 'izui-overlay'); 17 + 18 + izuiValue.textContent = currentIzuiState; 19 + izuiValue.classList.add(`izui-${currentIzuiState}`); 20 + 21 + debug && console.log('[hud:izui] Updated:', currentIzuiState); 22 + }; 23 + 24 + const refreshIzuiState = async () => { 25 + try { 26 + const state = await api.izui.getState(); 27 + if (state) { 28 + currentIzuiState = state; 29 + updateDisplay(); 30 + } 31 + } catch (error) { 32 + console.error('[hud:izui] Error:', error); 33 + } 34 + }; 35 + 36 + const init = async () => { 37 + await refreshIzuiState(); 38 + 39 + // Subscribe to IZUI state changes 40 + api.subscribe('izui:state-changed', (msg) => { 41 + if (msg.state) { 42 + currentIzuiState = msg.state; 43 + updateDisplay(); 44 + } 45 + }, api.scopes.GLOBAL); 46 + 47 + // Periodic fallback 48 + setInterval(refreshIzuiState, 5000); 49 + }; 50 + 51 + init().catch(error => console.error('[hud:izui] Init error:', error));
+22
extensions/hud/widgets/mode.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"> 6 + <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"> 7 + <title>Mode</title> 8 + <link rel="stylesheet" type="text/css" href="widget.css"> 9 + </head> 10 + <body> 11 + <div class="widget"> 12 + <div id="group-accent" class="group-accent" style="display: none;"></div> 13 + <div id="group-banner" class="group-banner" style="display: none;"> 14 + <div id="group-name" class="group-name"></div> 15 + </div> 16 + <div class="widget-label">Mode</div> 17 + <div id="mode-value" class="widget-value">-</div> 18 + </div> 19 + 20 + <script type="module" src="mode.js"></script> 21 + </body> 22 + </html>
+87
extensions/hud/widgets/mode.js
··· 1 + /** 2 + * Mode Widget — displays current mode and group context 3 + */ 4 + 5 + const api = window.app; 6 + const debug = api.debug; 7 + 8 + const modeValue = document.getElementById('mode-value'); 9 + const groupAccent = document.getElementById('group-accent'); 10 + const groupBanner = document.getElementById('group-banner'); 11 + const groupName = document.getElementById('group-name'); 12 + const widget = document.querySelector('.widget'); 13 + 14 + let currentMode = 'default'; 15 + let currentModeMetadata = {}; 16 + 17 + const updateDisplay = () => { 18 + if (!modeValue) return; 19 + 20 + // Remove old mode classes 21 + modeValue.classList.remove('mode-default', 'mode-page', 'mode-group', 'mode-settings'); 22 + 23 + // Format mode display 24 + let displayText = currentMode; 25 + if (currentMode === 'group' && currentModeMetadata.groupName) { 26 + displayText = `group: ${currentModeMetadata.groupName}`; 27 + } 28 + 29 + modeValue.textContent = displayText; 30 + modeValue.classList.add(`mode-${currentMode}`); 31 + 32 + // Update group visual treatment 33 + if (currentMode === 'group' && currentModeMetadata.groupName) { 34 + const color = currentModeMetadata.color || '#999'; 35 + groupAccent.style.background = color; 36 + groupAccent.style.display = ''; 37 + groupName.textContent = currentModeMetadata.groupName; 38 + groupBanner.style.display = ''; 39 + if (widget) widget.style.setProperty('--group-color', color + '40'); 40 + } else { 41 + groupAccent.style.display = 'none'; 42 + groupBanner.style.display = 'none'; 43 + if (widget) widget.style.removeProperty('--group-color'); 44 + } 45 + 46 + debug && console.log('[hud:mode] Updated:', displayText); 47 + }; 48 + 49 + const refreshMode = async () => { 50 + try { 51 + const focusedWindowId = await api.window.getFocusedVisibleWindowId(); 52 + const result = await api.context.get('mode', focusedWindowId); 53 + if (result.success && result.data) { 54 + currentMode = result.data.value || 'default'; 55 + currentModeMetadata = result.data.metadata || {}; 56 + } else { 57 + currentMode = 'default'; 58 + currentModeMetadata = {}; 59 + } 60 + updateDisplay(); 61 + } catch (error) { 62 + console.error('[hud:mode] Error:', error); 63 + } 64 + }; 65 + 66 + const init = async () => { 67 + await refreshMode(); 68 + 69 + // Watch for mode changes 70 + api.context.watchMode((mode, entry) => { 71 + if (mode) { 72 + currentMode = mode; 73 + currentModeMetadata = entry?.metadata || {}; 74 + updateDisplay(); 75 + } 76 + }); 77 + 78 + // Refresh on window focus changes 79 + api.subscribe('window:focused', () => refreshMode(), api.scopes.GLOBAL); 80 + api.subscribe('window:opened', () => refreshMode(), api.scopes.GLOBAL); 81 + api.subscribe('window:closed', () => refreshMode(), api.scopes.GLOBAL); 82 + 83 + // Periodic fallback 84 + setInterval(refreshMode, 5000); 85 + }; 86 + 87 + init().catch(error => console.error('[hud:mode] Init error:', error));
+18
extensions/hud/widgets/stats.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"> 6 + <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"> 7 + <title>Stats</title> 8 + <link rel="stylesheet" type="text/css" href="widget.css"> 9 + </head> 10 + <body> 11 + <div class="widget"> 12 + <div class="widget-label">Stats</div> 13 + <div id="stats-value" class="widget-value widget-value-small">-</div> 14 + </div> 15 + 16 + <script type="module" src="stats.js"></script> 17 + </body> 18 + </html>
+46
extensions/hud/widgets/stats.js
··· 1 + /** 2 + * Stats Widget — displays window count and other statistics 3 + */ 4 + 5 + const api = window.app; 6 + const debug = api.debug; 7 + 8 + const statsValue = document.getElementById('stats-value'); 9 + 10 + let windowCount = 0; 11 + 12 + const updateDisplay = () => { 13 + if (!statsValue) return; 14 + 15 + const parts = []; 16 + parts.push(`${windowCount} window${windowCount !== 1 ? 's' : ''}`); 17 + 18 + statsValue.textContent = parts.join(' \u2022 '); 19 + 20 + debug && console.log('[hud:stats] Updated:', windowCount); 21 + }; 22 + 23 + const refreshStats = async () => { 24 + try { 25 + const windowsResult = await api.window.list(); 26 + if (windowsResult.success && windowsResult.windows) { 27 + windowCount = windowsResult.windows.length; 28 + updateDisplay(); 29 + } 30 + } catch (error) { 31 + console.error('[hud:stats] Error:', error); 32 + } 33 + }; 34 + 35 + const init = async () => { 36 + await refreshStats(); 37 + 38 + // Refresh on window events 39 + api.subscribe('window:opened', () => refreshStats(), api.scopes.GLOBAL); 40 + api.subscribe('window:closed', () => refreshStats(), api.scopes.GLOBAL); 41 + 42 + // Periodic fallback 43 + setInterval(refreshStats, 5000); 44 + }; 45 + 46 + init().catch(error => console.error('[hud:stats] Init error:', error));
+87
extensions/hud/widgets/widget.css
··· 1 + /* Shared styles for HUD widget pages */ 2 + @import url('peek://theme/variables.css'); 3 + 4 + * { 5 + margin: 0; 6 + padding: 0; 7 + box-sizing: border-box; 8 + } 9 + 10 + html { 11 + font-family: var(--theme-font-sans); 12 + -webkit-font-smoothing: antialiased; 13 + font-size: 12px; 14 + line-height: 1.5; 15 + } 16 + 17 + html, body { 18 + width: 100%; 19 + height: 100%; 20 + overflow: hidden; 21 + background: transparent; 22 + } 23 + 24 + .widget { 25 + width: 100%; 26 + height: 100%; 27 + padding: 8px 10px; 28 + color: rgba(255, 255, 255, 0.45); 29 + font-size: 11px; 30 + } 31 + 32 + .widget-label { 33 + font-size: 9px; 34 + font-weight: 500; 35 + text-transform: uppercase; 36 + letter-spacing: 0.5px; 37 + color: rgba(255, 255, 255, 0.25); 38 + margin-bottom: 2px; 39 + } 40 + 41 + .widget-value { 42 + font-size: 12px; 43 + font-weight: 400; 44 + color: rgba(255, 255, 255, 0.45); 45 + line-height: 1.4; 46 + } 47 + 48 + .widget-value-truncate { 49 + white-space: nowrap; 50 + overflow: hidden; 51 + text-overflow: ellipsis; 52 + max-width: 100%; 53 + } 54 + 55 + .widget-value-small { 56 + font-size: 11px; 57 + line-height: 1.5; 58 + } 59 + 60 + /* Group mode — elevated visual treatment */ 61 + .mode-group { 62 + color: rgba(255, 255, 255, 0.7); 63 + font-weight: 500; 64 + } 65 + 66 + /* Group accent stripe */ 67 + .group-accent { 68 + height: 3px; 69 + border-radius: 3px 3px 0 0; 70 + margin: -8px -10px 6px -10px; 71 + background: #999; 72 + } 73 + 74 + /* Group banner */ 75 + .group-banner { 76 + margin-bottom: 4px; 77 + } 78 + 79 + .group-name { 80 + font-size: 13px; 81 + font-weight: 600; 82 + color: rgba(255, 255, 255, 0.75); 83 + letter-spacing: 0.2px; 84 + white-space: nowrap; 85 + overflow: hidden; 86 + text-overflow: ellipsis; 87 + }
+18
extensions/hud/widgets/window.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"> 6 + <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"> 7 + <title>Active Window</title> 8 + <link rel="stylesheet" type="text/css" href="widget.css"> 9 + </head> 10 + <body> 11 + <div class="widget"> 12 + <div class="widget-label">Active Window</div> 13 + <div id="window-title" class="widget-value widget-value-truncate">-</div> 14 + </div> 15 + 16 + <script type="module" src="window.js"></script> 17 + </body> 18 + </html>
+64
extensions/hud/widgets/window.js
··· 1 + /** 2 + * Active Window Widget — displays title of focused visible window 3 + */ 4 + 5 + const api = window.app; 6 + const debug = api.debug; 7 + 8 + const windowTitle = document.getElementById('window-title'); 9 + 10 + let currentWindowInfo = null; 11 + 12 + const updateDisplay = () => { 13 + if (!windowTitle) return; 14 + 15 + if (currentWindowInfo && currentWindowInfo.title) { 16 + windowTitle.textContent = currentWindowInfo.title; 17 + } else { 18 + windowTitle.textContent = 'No active window'; 19 + } 20 + 21 + debug && console.log('[hud:window] Updated:', currentWindowInfo?.title); 22 + }; 23 + 24 + const refreshWindowInfo = async () => { 25 + try { 26 + const [windowsResult, focusedWindowId] = await Promise.all([ 27 + api.window.list(), 28 + api.window.getFocusedVisibleWindowId() 29 + ]); 30 + 31 + if (windowsResult.success && windowsResult.windows) { 32 + const focusedWindow = focusedWindowId 33 + ? windowsResult.windows.find(w => w.id === focusedWindowId) 34 + : null; 35 + 36 + if (focusedWindow) { 37 + currentWindowInfo = { 38 + title: focusedWindow.title || 'Untitled', 39 + url: focusedWindow.url || focusedWindow.source 40 + }; 41 + } else { 42 + currentWindowInfo = null; 43 + } 44 + 45 + updateDisplay(); 46 + } 47 + } catch (error) { 48 + console.error('[hud:window] Error:', error); 49 + } 50 + }; 51 + 52 + const init = async () => { 53 + await refreshWindowInfo(); 54 + 55 + // Refresh on window events 56 + api.subscribe('window:focused', () => refreshWindowInfo(), api.scopes.GLOBAL); 57 + api.subscribe('window:opened', () => refreshWindowInfo(), api.scopes.GLOBAL); 58 + api.subscribe('window:closed', () => refreshWindowInfo(), api.scopes.GLOBAL); 59 + 60 + // Periodic fallback 61 + setInterval(refreshWindowInfo, 5000); 62 + }; 63 + 64 + init().catch(error => console.error('[hud:window] Init error:', error));