Last active
March 7, 2025 16:39
-
-
Save nicholaswmin/dcb80c1bc51797a4bab13980992993b1 to your computer and use it in GitHub Desktop.
update a chrome tab without losing focus; macos-only
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/**l | |
* Opens Chrome tab to a specified URL(or reload if exists) | |
* without the usual ballbusting focus loss on every file change. | |
* | |
* Meant for: | |
* `$ node --watch` or `$ nodemon app.js` | |
* type of orkflows where you don't | |
* want each file-save taking you to the browser. | |
* | |
* macOS only; no Linux/Windows | |
* | |
* authors: @nicholasmwin, license: MIT | |
* dependencies | |
* - chrome-cli v1.9.3, github.com/prasmussen/chrome-cli | |
* - creates window/tab if needed | |
* - reloads quietly (no focus loss) | |
* | |
* @example | |
* import { browzen } from 'browzen' | |
* | |
* // assumes the standard server/app listener | |
* | |
* const server = http.listen(port, () => { | |
* const addr = server.address() | |
* browzen(`http://${['::','0.0.0.0'].includes(addr.address) | |
* ? 'localhost' | |
* : addr.address}:${addr.port}`, addr.port) | |
* }) | |
*/ | |
export const browzen = async (url, port = null) => { | |
const log = (color, msg) => console.log(`\x1b[${color}m${msg}\x1b[0m`) | |
log(32, `server: ${url}`) | |
// Setup exec function | |
const setupExec = async () => { | |
try { | |
const { exec: localExecCb } = await import('child_process') | |
const { promisify: localPromisify } = await import('util') | |
return localPromisify(localExecCb) | |
} catch { | |
log(31, `missing required imports: child_process, util`) | |
log(2, `open url in chrome:`) | |
console.log(url) | |
return null | |
} | |
} | |
const exec = await setupExec() | |
if (!exec) return | |
// Extract port if needed | |
const resolvedPort = port || url.match(/:(\d+)/)?.at(1) || null | |
// Pure function utilities | |
const getId = text => text.match(/\[(.*?)\]/)?.at(1) ?? null | |
const isExpectedStartupError = err => | |
err.message?.includes('Failed to connect') | |
const waitFor = (ms = 100) => new Promise(resolve => setTimeout(resolve, ms)) | |
// Chrome interaction functions | |
const getWindows = () => | |
exec('chrome-cli list windows') | |
.then(({ stdout }) => stdout.trim() ? stdout.split('\n') : []) | |
.catch(() => []) | |
const getFirstWindowId = () => | |
getWindows() | |
.then(windows => windows.length ? getId(windows[0]) : null) | |
const findTab = async (url, port, winId) => { | |
const pattern = port ? `:${port}/` : url | |
try { | |
const { stdout } = await exec('chrome-cli list links') | |
const lines = stdout.split('\n') | |
const matchingLine = lines.find(line => line?.includes(pattern)) | |
return { | |
winId, | |
tabId: matchingLine ? getId(matchingLine) : null | |
} | |
} catch { | |
return { winId, tabId: null } | |
} | |
} | |
const isCommandAvailable = cmd => | |
exec(`which ${cmd}`).then(() => true).catch(() => false) | |
const reloadTab = async tabId => { | |
await exec(`chrome-cli reload -t ${tabId}`) | |
log(2, `refreshed tab`) | |
} | |
const openTab = async (url, winId) => { | |
await exec(`chrome-cli open "${url}" -w ${winId}`) | |
log(2, `opened new tab`) | |
} | |
const openChromeWindow = () => | |
exec('open -a "Google Chrome" --args --new-window about:blank') | |
// Recursive poll for window with proper error handling | |
const pollForWindow = async (attempts = 20, interval = 100) => { | |
// Base case - no more attempts | |
if (attempts <= 0) return null | |
try { | |
const winId = await getFirstWindowId() | |
if (winId) return winId | |
} catch (err) { | |
if (!isExpectedStartupError(err)) { | |
throw err | |
} | |
} | |
// Recursive case - wait and try again | |
await waitFor(interval) | |
return pollForWindow(attempts - 1, interval) | |
} | |
// Main workflow | |
const openChrome = async () => { | |
// Check for chrome-cli | |
if (!(await isCommandAvailable('chrome-cli'))) { | |
log(31, `chrome-cli not found`) | |
log(2, `brew install chrome-cli`) | |
log(2, `open url in chrome:`) | |
console.log(url) | |
return | |
} | |
// Get or create window | |
let winId = await getFirstWindowId() | |
if (!winId) { | |
await openChromeWindow() | |
log(2, `opening chrome window`) | |
winId = await pollForWindow() | |
if (!winId) throw new Error('failed to open chrome window') | |
log(2, `opened new window`) | |
} | |
// Find or create tab | |
const { tabId } = await findTab(url, resolvedPort, winId) | |
// Final action | |
return tabId | |
? await reloadTab(tabId) | |
: await openTab(url, winId) | |
} | |
// Execute with error handling | |
try { | |
await openChrome() | |
} catch (err) { | |
log(31, `error: ${err.message || err}`) | |
log(2, `open url in chrome:`) | |
console.log(url) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "browzen", | |
"version": "0.3.0", | |
"description": "refresh a chrome tab w.o losing focus", | |
"keywords": [ | |
"chrome-cli", | |
"chrome" | |
], | |
"repository": { | |
"type": "git", | |
"url": "https://gist.github.com/nicholaswmin/dcb80c1bc51797a4bab13980992993b1" | |
}, | |
"license": "MIT", | |
"author": "Nicholas Kyriakides <[email protected]> (https://github.io/nicholaswmin)", | |
"type": "module", | |
"main": "browzen.js" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment