Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 30 additions & 24 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
VERSION := $(shell grep -o '"version": *"[^"]*"' src/manifest.firefox.json | grep -o '[0-9][0-9.]*')

.PHONY: all clean build build-firefox build-chrome lint run

all: clean build

# clean
clean:
rm -rf dist/*

# build
build:
mkdir -p dist/{firefox,chrome}

# copy manifest.json
cp src/manifest.json dist/firefox
cp src/manifest.json dist/chrome

# copy background.js
cp src/background.js dist/firefox
cp src/background.js dist/chrome

# copy popup
cp -rf src/popup dist/firefox
cp -rf src/popup dist/chrome

# copy icons
cp -rf src/icons dist/firefox
cp -rf src/icons dist/chrome

# zip the folders
(cd ./dist/firefox/; zip -rm power-close.zip *)
(cd ./dist/chrome/; zip -rm power-close.zip *)
build: build-firefox build-chrome

build-firefox:
mkdir -p dist/firefox
cp src/manifest.firefox.json dist/firefox/manifest.json
cp src/background.js dist/firefox/
cp -rf src/popup dist/firefox/
cp -rf src/icons dist/firefox/
cp -rf src/options dist/firefox/
(cd dist/firefox && zip -rm ../power-close-$(VERSION)-firefox.zip *)

build-chrome:
mkdir -p dist/chrome
cp src/manifest.chrome.json dist/chrome/manifest.json
cp src/background.js dist/chrome/
cp -rf src/popup dist/chrome/
cp -rf src/icons dist/chrome/
cp -rf src/options dist/chrome/
(cd dist/chrome && zip -rm ../power-close-$(VERSION)-chrome.zip *)

# Requires: npm install -g web-ext
lint: build-firefox
web-ext lint --source-dir=dist/firefox

run: build-firefox
web-ext run --source-dir=dist/firefox
119 changes: 97 additions & 22 deletions src/background.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,107 @@
(function () {
'use strict';
function closeTabsByKeyword(keyword) {
if (!keyword) {
return;
'use strict';

const DEFAULT_PREFS = {
excludeCurrentTab: true,
excludePinnedTabs: true,
excludeExtensionPages: true,
caseSensitive: false,
};

const INTERNAL_SCHEMES = [
'chrome://',
'about:',
'edge://',
'opera://',
'vivaldi://',
'brave://',
];

function isInternalUrl(url) {
if (!url) return true;
return INTERNAL_SCHEMES.some(scheme => url.startsWith(scheme));
}

function isExtensionUrl(url) {
return url.startsWith('chrome-extension://') || url.startsWith('moz-extension://');
}

async function getPrefs() {
return chrome.storage.sync.get(DEFAULT_PREFS);
}

async function closeTabsByKeyword(keyword, sourceTabId) {
if (!keyword) return;

const prefs = await getPrefs();
const needle = prefs.caseSensitive ? keyword : keyword.toLowerCase();
const tabs = await chrome.tabs.query({});
const toClose = [];

for (const tab of tabs) {
if (isInternalUrl(tab.url)) continue;
if (prefs.excludeExtensionPages && isExtensionUrl(tab.url)) continue;
if (prefs.excludePinnedTabs && tab.pinned) continue;
if (prefs.excludeCurrentTab && tab.id === sourceTabId) continue;

const haystack = prefs.caseSensitive ? tab.url : tab.url.toLowerCase();
if (haystack.includes(needle)) {
toClose.push(tab.id);
}
}

chrome.tabs.query({}, function (tabs) {
for (const tab of tabs) {
if (tab.url.indexOf(keyword) !== -1) {
chrome.tabs.remove(tab.id);
}
}
});
if (toClose.length > 0) {
chrome.tabs.remove(toClose);
}
}

async function closeDuplicateTabs(sourceTabId) {
const prefs = await getPrefs();
const tabs = await chrome.tabs.query({});
const seen = new Set();
const toClose = [];

chrome.runtime.onMessage.addListener(function (response) {
if (response.event !== 'text-entered') {
return;
for (const tab of tabs) {
if (isInternalUrl(tab.url)) continue;
if (prefs.excludePinnedTabs && tab.pinned) continue;
if (prefs.excludeCurrentTab && tab.id === sourceTabId) continue;

if (seen.has(tab.url)) {
toClose.push(tab.id);
} else {
seen.add(tab.url);
}
}

closeTabsByKeyword(response.text);
});
if (toClose.length > 0) {
chrome.tabs.remove(toClose);
}
}

chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'close-domain',
title: 'Close all tabs from this domain',
onclick: function (event) {
const keyword = new URL(event.pageUrl).hostname;
closeTabsByKeyword(keyword);
}
contexts: ['page', 'image', 'link', 'frame'],
});
chrome.contextMenus.create({
id: 'close-duplicates',
title: 'Close duplicate tabs',
contexts: ['page', 'image', 'link', 'frame'],
});
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === 'close-domain') {
const keyword = new URL(info.pageUrl).hostname;
closeTabsByKeyword(keyword, tab?.id);
} else if (info.menuItemId === 'close-duplicates') {
closeDuplicateTabs(tab?.id);
}
});

chrome.runtime.onMessage.addListener((message) => {
if (message.event !== 'text-entered') return;
chrome.tabs.query({ active: true, currentWindow: true }, (activeTabs) => {
closeTabsByKeyword(message.text, activeTabs[0]?.id);
});
})();
});
30 changes: 21 additions & 9 deletions src/manifest.json → src/manifest.chrome.json
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
{
"manifest_version": 3,
"name": "Power Close",
"version": "1.0.1",
"version": "1.1.0",
"description": "A browser extension, for the tab addict, to close all tabs with urls containing a keyword or a domain name.",
"author": "Mosab Ibrahim",
"homepage_url": "https://github.com/ArabesqueLabs/power-close",
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"browser_action": {
"action": {
"default_icon": {
"19": "icons/19.png",
"38": "icons/38.png"
},
"default_title": "Power Close",
"default_popup": "popup/popup.html"
},
"author": "Mosab Ibrahim",
"background": {
"scripts": [
"background.js"
]
"service_worker": "background.js"
},
"homepage_url": "https://github.com/mos3abof/firefox-power-close",
"permissions": [
"tabs",
"contextMenus"
]
"contextMenus",
"storage"
],
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+X",
"mac": "Command+Shift+X"
},
"description": "Open Power Close"
}
},
"options_ui": {
"page": "options/options.html",
"open_in_tab": false
}
}
48 changes: 48 additions & 0 deletions src/manifest.firefox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"manifest_version": 3,
"name": "Power Close",
"version": "1.1.0",
"description": "A browser extension, for the tab addict, to close all tabs with urls containing a keyword or a domain name.",
"author": "Mosab Ibrahim",
"homepage_url": "https://github.com/ArabesqueLabs/power-close",
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"action": {
"default_icon": {
"19": "icons/19.png",
"38": "icons/38.png"
},
"default_title": "Power Close",
"default_popup": "popup/popup.html"
},
"background": {
"scripts": ["background.js"]
},
"permissions": [
"tabs",
"contextMenus",
"storage"
],
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+X",
"mac": "Command+Shift+X"
},
"description": "Open Power Close"
}
},
"options_ui": {
"page": "options/options.html",
"open_in_tab": false
},
"browser_specific_settings": {
"gecko": {
"id": "power-close@arabesquelabs",
"strict_min_version": "109.0"
}
}
}
77 changes: 77 additions & 0 deletions src/options/options.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
*, *::before, *::after {
box-sizing: border-box;
}

body {
font-family: system-ui, sans-serif;
font-size: 0.9rem;
color: #1f2937;
padding: 1.5rem 2rem;
max-width: 400px;
margin: 0;
}

h1 {
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1.5rem;
}

fieldset {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 0.75rem 1rem 1rem;
margin: 0 0 1rem;
}

legend {
font-weight: 600;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
padding: 0 0.25rem;
}

label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.3rem 0;
}

input[type="checkbox"] {
accent-color: #6366f1;
width: 15px;
height: 15px;
cursor: pointer;
}

.actions {
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 0.5rem;
}

button {
padding: 0.4rem 1.25rem;
background: #6366f1;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}

button:hover {
background: #4f46e5;
}

#status {
font-size: 0.8rem;
color: #22c55e;
font-weight: 500;
}
Loading