Style Holesail tunnel browser with peardock titlebar and pear-ctrl
Release rolling / release (push) Successful in 7m37s

Open local tunnel URLs in a framed Electron window that matches main app
chrome (hiddenInset/frameless, pear-ctrl, brand) instead of a bare popup.
This commit is contained in:
Raven Scott
2026-07-17 20:26:51 -04:00
parent dc3d4ac872
commit e20ae7b41f
2 changed files with 783 additions and 24 deletions
+300 -24
View File
@@ -14,7 +14,7 @@
*/
'use strict'
const { app, BrowserWindow, ipcMain, session, Menu } = require('electron')
const { app, BrowserWindow, BrowserView, ipcMain, session, Menu } = require('electron')
const path = require('path')
const fs = require('fs')
const os = require('os')
@@ -223,12 +223,10 @@ function resolveAppIcon() {
return undefined
}
function createWindow() {
const icon = resolveAppIcon()
/** Shared window chrome: hiddenInset (macOS) / frameless (win/linux) + pear-ctrl. */
function windowChromeOpts() {
const isDarwin = process.platform === 'darwin'
// Match Pear GUI chrome: no separate native title strip — traffic lights /
// window buttons live inside #titlebar (see pear-ctrl in preload + CSS).
const chromeOpts = isDarwin
return isDarwin
? {
titleBarStyle: 'hiddenInset',
// Align OS traffic lights with 42px #titlebar (see --titlebar-h)
@@ -238,7 +236,216 @@ function createWindow() {
// Windows / Linux: frameless; pear-ctrl renders min/max/close
frame: false,
}
}
function defaultWebPreferences() {
return {
preload: buildPreloadPath(),
// Full peardock UI imports HyperDHT etc. as ESM node packages — same as pear UI.
// Tunnel shell also needs nodeIntegration so the pear-ctrl polyfill / IPC works.
nodeIntegration: true,
contextIsolation: false,
sandbox: false,
spellcheck: false,
webSecurity: true,
}
}
/** @type {Map<string, import('electron').BrowserWindow>} */
const tunnelWindows = new Map()
function isLocalTunnelUrl(href) {
try {
const u = new URL(String(href || ''))
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false
const host = (u.hostname || '').toLowerCase()
return host === '127.0.0.1' || host === 'localhost' || host === '::1' || host === '[::1]'
} catch {
return false
}
}
/** Titlebar (42) + tunnel toolbar (6+28+6=40) — keep in sync with tunnel-browser.html */
const TUNNEL_CHROME_H = 42 + 40
/**
* Open (or focus) a peardock-styled BrowserWindow for a local Holesail tunnel URL.
* Shell HTML matches main #titlebar + pear-ctrl; tunnel content loads in a
* BrowserView (avoids X-Frame-Options blocking iframes).
* @param {string} targetUrl
* @param {{ title?: string }} [opts]
*/
function createTunnelWindow(targetUrl, opts = {}) {
const href = String(targetUrl || '').trim()
if (!href || !isLocalTunnelUrl(href)) {
console.warn('[peardock] refused non-local tunnel URL:', href)
return null
}
if (!staticPort) {
console.warn('[peardock] static server not ready; cannot open tunnel browser')
return null
}
const existing = tunnelWindows.get(href)
if (existing && !existing.isDestroyed()) {
if (existing.isMinimized()) existing.restore()
existing.focus()
return existing
}
const icon = resolveAppIcon()
const titleHint = String(opts.title || '').trim()
const win = new BrowserWindow({
width: 1100,
height: 800,
minWidth: 640,
minHeight: 420,
backgroundColor: pkg.pear?.gui?.backgroundColor || '#0f1117',
title: titleHint ? `${titleHint} — peardock tunnel` : 'Holesail tunnel — peardock',
...(icon ? { icon } : {}),
autoHideMenuBar: process.platform !== 'darwin',
...windowChromeOpts(),
webPreferences: defaultWebPreferences(),
show: false,
})
// Content view: real navigation (no iframe / X-Frame-Options issues)
const view = new BrowserView({
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
spellcheck: false,
webSecurity: true,
},
})
win.setBrowserView(view)
/** @type {{ view: import('electron').BrowserView, url: string }} */
win.__pdTunnel = { view, url: href }
const layoutView = () => {
if (win.isDestroyed()) return
const [width, height] = win.getContentSize()
view.setBounds({
x: 0,
y: TUNNEL_CHROME_H,
width,
height: Math.max(0, height - TUNNEL_CHROME_H),
})
}
layoutView()
win.on('resize', layoutView)
const pushNavState = () => {
if (win.isDestroyed()) return
const wc = view.webContents
const state = {
url: wc.getURL() || href,
title: wc.getTitle() || titleHint || 'Holesail',
canGoBack: wc.canGoBack(),
canGoForward: wc.canGoForward(),
loading: wc.isLoading(),
}
try {
win.webContents.send('peardock:tunnel-nav-state', state)
} catch {
// shell not ready
}
try {
if (state.title) win.setTitle(`${state.title} — peardock tunnel`)
} catch {
// ignore
}
}
view.webContents.on('did-start-loading', pushNavState)
view.webContents.on('did-stop-loading', pushNavState)
view.webContents.on('did-navigate', pushNavState)
view.webContents.on('did-navigate-in-page', pushNavState)
view.webContents.on('page-title-updated', pushNavState)
view.webContents.on('did-fail-load', (_e, code, desc, validatedURL, isMainFrame) => {
if (!isMainFrame) return
try {
win.webContents.send('peardock:tunnel-nav-state', {
url: validatedURL || href,
title: 'Failed to load',
canGoBack: view.webContents.canGoBack(),
canGoForward: view.webContents.canGoForward(),
loading: false,
error: desc || `Error ${code}`,
})
} catch {
// ignore
}
})
view.webContents.setWindowOpenHandler(({ url }) => {
if (isLocalTunnelUrl(url)) {
createTunnelWindow(url)
return { action: 'deny' }
}
return { action: 'deny' }
})
tunnelWindows.set(href, win)
win.on('closed', () => {
if (tunnelWindows.get(href) === win) tunnelWindows.delete(href)
try {
win.setBrowserView(null)
} catch {
// ignore
}
})
win.once('ready-to-show', () => {
win.show()
layoutView()
})
const shell = new URL(`http://127.0.0.1:${staticPort}/tunnel-browser.html`)
shell.searchParams.set('url', href)
shell.searchParams.set('mode', 'view')
if (titleHint) shell.searchParams.set('title', titleHint)
win
.loadURL(shell.href)
.then(() => {
layoutView()
return view.webContents.loadURL(href)
})
.then(() => pushNavState())
.catch((err) => {
console.error('[peardock] failed to load tunnel browser:', err)
try {
win.webContents.send('peardock:tunnel-nav-state', {
url: href,
title: 'Failed to load',
canGoBack: false,
canGoForward: false,
loading: false,
error: String(err?.message || err),
})
} catch {
// ignore
}
})
// Nested window.open from shell chrome only
win.webContents.setWindowOpenHandler(({ url }) => {
if (isLocalTunnelUrl(url)) {
createTunnelWindow(url)
return { action: 'deny' }
}
return { action: 'deny' }
})
return win
}
function createWindow() {
const icon = resolveAppIcon()
// Match Pear GUI chrome: no separate native title strip — traffic lights /
// window buttons live inside #titlebar (see pear-ctrl in preload + CSS).
const win = new BrowserWindow({
width: pkg.pear?.gui?.width || 1280,
height: pkg.pear?.gui?.height || 800,
@@ -248,16 +455,8 @@ function createWindow() {
// Hide the default Electron menu bar (File/Edit/View/…) on Linux/Windows.
// macOS keeps the system menu bar; non-darwin has no app menu (set in whenReady).
autoHideMenuBar: process.platform !== 'darwin',
...chromeOpts,
webPreferences: {
preload: buildPreloadPath(),
// Full peardock UI imports HyperDHT etc. as ESM node packages — same as pear UI.
nodeIntegration: true,
contextIsolation: false,
sandbox: false,
spellcheck: false,
webSecurity: true,
},
...windowChromeOpts(),
webPreferences: defaultWebPreferences(),
show: false,
})
@@ -273,6 +472,40 @@ function createWindow() {
console.error('did-fail-load', code, desc)
})
// Holesail tryOpenBrowser / window.open → styled tunnel shell (not bare Chromium popup)
win.webContents.setWindowOpenHandler(({ url }) => {
try {
const u = new URL(url)
// Direct open of our shell page: allow with matching chrome
if (
u.hostname === '127.0.0.1' &&
u.port === String(staticPort) &&
u.pathname.endsWith('/tunnel-browser.html')
) {
return {
action: 'allow',
overrideBrowserWindowOptions: {
width: 1100,
height: 800,
minWidth: 640,
minHeight: 420,
backgroundColor: pkg.pear?.gui?.backgroundColor || '#0f1117',
autoHideMenuBar: process.platform !== 'darwin',
...windowChromeOpts(),
webPreferences: defaultWebPreferences(),
},
}
}
if (isLocalTunnelUrl(url)) {
createTunnelWindow(url)
return { action: 'deny' }
}
} catch {
// ignore
}
return { action: 'deny' }
})
return win
}
@@ -291,27 +524,70 @@ ipcMain.handle('peardock:exit', () => {
})
/** Window chrome controls for frameless (win/linux) and general shell actions */
function windowFromEvent(evt) {
try {
return BrowserWindow.fromWebContents(evt.sender)
} catch {
return null
}
}
function focusedWindow() {
return BrowserWindow.getFocusedWindow() || BrowserWindow.getAllWindows()[0] || null
}
ipcMain.handle('peardock:window-minimize', () => {
focusedWindow()?.minimize()
ipcMain.handle('peardock:window-minimize', (evt) => {
;(windowFromEvent(evt) || focusedWindow())?.minimize()
})
ipcMain.handle('peardock:window-maximize', () => {
const win = focusedWindow()
ipcMain.handle('peardock:window-maximize', (evt) => {
const win = windowFromEvent(evt) || focusedWindow()
if (!win) return
if (win.isMaximized()) win.unmaximize()
else win.maximize()
})
ipcMain.handle('peardock:window-close', () => {
focusedWindow()?.close()
ipcMain.handle('peardock:window-close', (evt) => {
;(windowFromEvent(evt) || focusedWindow())?.close()
})
ipcMain.handle('peardock:window-is-maximized', () => {
return Boolean(focusedWindow()?.isMaximized())
ipcMain.handle('peardock:window-is-maximized', (evt) => {
return Boolean((windowFromEvent(evt) || focusedWindow())?.isMaximized())
})
/**
* Open a Holesail tunnel in a peardock-styled BrowserWindow
* (matching main #titlebar + pear-ctrl). Used by client/holesailLocal.js.
*/
ipcMain.handle('peardock:open-tunnel-browser', (_evt, payload) => {
try {
const url = typeof payload === 'string' ? payload : payload?.url
const title = typeof payload === 'object' && payload ? payload.title : undefined
const win = createTunnelWindow(url, { title })
return Boolean(win)
} catch (err) {
console.error('[peardock] open-tunnel-browser failed:', err)
return false
}
})
/** Toolbar actions from tunnel-browser.html (BrowserView mode) */
ipcMain.handle('peardock:tunnel-nav', (evt, action) => {
try {
const win = BrowserWindow.fromWebContents(evt.sender)
const view = win?.__pdTunnel?.view
if (!win || !view || view.webContents.isDestroyed()) return false
const act = String(action || '')
if (act === 'back' && view.webContents.canGoBack()) view.webContents.goBack()
else if (act === 'forward' && view.webContents.canGoForward()) view.webContents.goForward()
else if (act === 'reload') view.webContents.reload()
else if (act === 'stop') view.webContents.stop()
else return false
return true
} catch (err) {
console.error('[peardock] tunnel-nav failed:', err)
return false
}
})