commit f0f3a7e4eac9cb30d546a24df295c49ae807911c Author: Lukas Bauer Date: Thu Jun 12 20:00:00 2025 +0000 Initial commit — basic tab grouping works diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb0956b --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# tab-tidy + +A small browser extension that groups and sorts your open tabs by domain. +Built for Firefox. Side project — scratching my own itch. + +## Install (dev) + + npm install + npm run build + # Load dist/ as a temporary extension in about:debugging + +## Features + +- Groups tabs by domain +- Sorts groups alphabetically or by last-used +- One-click close for all tabs in a group +- Keyboard shortcut: Alt+T to open the popup + +## Status + +Works for me™. Not on the extension store yet. diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..0ba4a86 --- /dev/null +++ b/manifest.json @@ -0,0 +1,21 @@ +{ + "manifest_version": 3, + "name": "Tab Tidy", + "version": "0.3.1", + "description": "Group and sort your tabs by domain", + "permissions": ["tabs"], + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png" + } + }, + "commands": { + "_execute_action": { + "suggested_key": { + "default": "Alt+T" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c251220 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "tab-tidy", + "version": "0.3.1", + "description": "Group and sort browser tabs by domain", + "scripts": { + "build": "webpack --config webpack.config.js", + "dev": "webpack --config webpack.config.js --watch", + "lint": "eslint src/" + }, + "devDependencies": { + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4", + "eslint": "^8.57.0", + "copy-webpack-plugin": "^12.0.2" + } +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..9a83f0e --- /dev/null +++ b/popup.html @@ -0,0 +1,18 @@ + + + + + Tab Tidy + + + +
+ + + diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..54406b4 --- /dev/null +++ b/src/background.js @@ -0,0 +1,12 @@ +// Background service worker +// Listens for tab updates and triggers a re-group + +chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { + if (changeInfo.status === 'complete') { + chrome.runtime.sendMessage({ type: 'TABS_CHANGED' }).catch(() => {}); + } +}); + +chrome.tabs.onRemoved.addListener(() => { + chrome.runtime.sendMessage({ type: 'TABS_CHANGED' }).catch(() => {}); +}); diff --git a/src/popup.js b/src/popup.js new file mode 100644 index 0000000..b6ecd38 --- /dev/null +++ b/src/popup.js @@ -0,0 +1,49 @@ +const groupByDomain = (tabs) => { + return tabs.reduce((groups, tab) => { + try { + const domain = new URL(tab.url).hostname; + if (!groups[domain]) groups[domain] = []; + groups[domain].push(tab); + } catch (_) {} + return groups; + }, {}); +}; + +const render = async () => { + const tabs = await chrome.tabs.query({ currentWindow: true }); + const groups = groupByDomain(tabs); + const sorted = Object.entries(groups).sort(([a], [b]) => a.localeCompare(b)); + + const container = document.getElementById('groups'); + container.innerHTML = ''; + + for (const [domain, domainTabs] of sorted) { + const section = document.createElement('section'); + const header = document.createElement('h2'); + header.textContent = `${domain} (${domainTabs.length})`; + + const closeBtn = document.createElement('button'); + closeBtn.textContent = 'close all'; + closeBtn.onclick = () => { + chrome.tabs.remove(domainTabs.map(t => t.id)).then(render); + }; + + header.appendChild(closeBtn); + section.appendChild(header); + + for (const tab of domainTabs) { + const item = document.createElement('div'); + item.className = 'tab-item'; + item.textContent = tab.title || tab.url; + item.title = tab.url; + item.onclick = () => chrome.tabs.update(tab.id, { active: true }); + section.appendChild(item); + } + container.appendChild(section); + } +}; + +document.addEventListener('DOMContentLoaded', render); +chrome.runtime.onMessage.addListener((msg) => { + if (msg.type === 'TABS_CHANGED') render(); +});