// ==UserScript==
// @name GitHub Advanced Search Builder
// @namespace https://github.com/quantavil/userscript
// @version 1.8
// @description Advanced filter modal for GitHub search with OR/AND/NOT logic and native look.
// @author quantavil
// @match https://github.com/*
// @license MIT
// @icon https://github.githubassets.com/favicons/favicon.svg
// @grant none
// ==/UserScript==
(function () {
'use strict';
// Config
const TRIGGER_ID = 'gh-adv-search-btn';
const MODAL_ID = 'gh-adv-search-modal';
// Icons
const FILTER_ICON = ``;
function createUI() {
if (document.getElementById(TRIGGER_ID)) return;
// Find the global search input container
const headerSearch = document.querySelector('.header-search-wrapper, .AppHeader-search');
if (!headerSearch) return;
// Create Trigger Button
const btn = document.createElement('button');
btn.id = TRIGGER_ID;
btn.className = 'btn btn-sm ml-2';
btn.style.display = 'inline-flex';
btn.style.alignItems = 'center';
btn.style.gap = '4px';
btn.innerHTML = `${FILTER_ICON} Filter`;
btn.title = "Advanced Search Builder (Ctrl+Shift+F)";
// Insert Button
if (headerSearch.parentNode) {
headerSearch.parentNode.insertBefore(btn, headerSearch.nextSibling);
}
// Create Modal (Hidden by default)
const modal = document.createElement('div');
modal.id = MODAL_ID;
modal.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 95%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
z-index: 9999;
background-color: var(--bgColor-default, #fff);
border: 1px solid var(--borderColor-default, #d0d7de);
border-radius: 6px;
box-shadow: var(--shadow-large, 0 8px 24px rgba(140,149,159,0.2));
display: none;
padding: 16px;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;
color: var(--fgColor-default, #24292f);
box-sizing: border-box;
`;
// Add responsive grid style
const style = document.createElement('style');
style.innerHTML = `
#${MODAL_ID} .responsive-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
@media (max-width: 480px) {
#${MODAL_ID} .responsive-grid {
grid-template-columns: 1fr;
}
#${MODAL_ID} {
top: 10px;
transform: translateX(-50%);
}
}
`;
document.head.appendChild(style);
modal.innerHTML = `
`;
document.body.appendChild(modal);
// Events
btn.addEventListener('click', (e) => {
e.preventDefault();
const isOpening = modal.style.display !== 'block';
modal.style.display = isOpening ? 'block' : 'none';
if (isOpening) {
populateFieldsFromURL();
document.getElementById('inp-and').focus();
}
});
document.getElementById(`${MODAL_ID}-close`).addEventListener('click', () => {
modal.style.display = 'none';
});
document.getElementById(`${MODAL_ID}-form`).addEventListener('submit', (e) => {
e.preventDefault();
executeSearch();
});
document.getElementById(`${MODAL_ID}-clear`).addEventListener('click', () => {
const ids = ['inp-and', 'inp-or', 'inp-not', 'inp-user', 'inp-repo', 'inp-lang', 'inp-ext', 'inp-stars', 'inp-forks', 'inp-path', 'inp-topics', 'inp-created', 'inp-pushed', 'inp-size', 'sel-type', 'sel-sort'];
ids.forEach(id => {
const el = document.getElementById(id);
if (el) {
if (el.tagName === 'SELECT') el.selectedIndex = 0;
else el.value = '';
}
});
});
// Close on escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') modal.style.display = 'none';
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
modal.style.display = 'block';
populateFieldsFromURL();
document.getElementById('inp-and').focus();
}
});
}
function populateFieldsFromURL() {
const params = new URLSearchParams(window.location.search);
const query = params.get('q');
const type = params.get('type');
const sort = params.get('s');
// Reset fields
const allIds = ['inp-and', 'inp-or', 'inp-not', 'inp-user', 'inp-repo', 'inp-lang', 'inp-ext', 'inp-stars', 'inp-forks', 'inp-path', 'inp-topics', 'inp-created', 'inp-pushed', 'inp-size', 'sel-type', 'sel-sort'];
allIds.forEach(id => {
const el = document.getElementById(id);
if (el) {
if (el.tagName === 'SELECT') el.selectedIndex = 0;
else el.value = '';
}
});
if (type) document.getElementById('sel-type').value = type;
if (sort) document.getElementById('sel-sort').value = sort;
if (!query) return;
let remainingQuery = query;
// 1. Extract metadata filters
const metadataMap = {
'user': 'inp-user',
'repo': 'inp-repo',
'language': 'inp-lang',
'extension': 'inp-ext',
'stars': 'inp-stars',
'forks': 'inp-forks',
'path': 'inp-path',
'topic': 'inp-topics',
'created': 'inp-created',
'pushed': 'inp-pushed',
'size': 'inp-size'
};
for (const [key, id] of Object.entries(metadataMap)) {
const regex = new RegExp(`${key}:(\\S+)`, 'i');
const match = remainingQuery.match(regex);
if (match) {
let val = match[1];
if (key === 'stars' || key === 'forks') {
val = val.replace('>=', '');
}
document.getElementById(id).value = val;
remainingQuery = remainingQuery.replace(match[0], '');
}
}
// 2. Extract OR groups: (A OR B OR C)
const orMatch = remainingQuery.match(/\(([^)]+ OR [^)]+)\)/i);
if (orMatch) {
const terms = orMatch[1].split(/\s+OR\s+/i);
document.getElementById('inp-or').value = terms.join(', ');
remainingQuery = remainingQuery.replace(orMatch[0], '');
}
// 3. Extract NOT terms: -term
const notTerms = [];
remainingQuery = remainingQuery.replace(/-(\S+)/g, (match, term) => {
notTerms.push(term);
return '';
});
if (notTerms.length > 0) {
document.getElementById('inp-not').value = notTerms.join(', ');
}
// 4. Remaining goes to AND
const andVal = remainingQuery.trim().replace(/\s+/g, ' ');
if (andVal) {
document.getElementById('inp-and').value = andVal;
}
}
function executeSearch() {
let queryParts = [];
const getVal = (id) => document.getElementById(id).value.trim();
// Helper to split by space, comma, or semicolon
const parseList = (val) => val.split(/[\s,;]+/).filter(t => t.length > 0);
// 1. Handle AND
const andVal = getVal('inp-and');
if (andVal) queryParts.push(andVal);
// 2. Handle OR
const orVal = getVal('inp-or');
if (orVal) {
const terms = parseList(orVal);
if (terms.length > 1) queryParts.push(`(${terms.join(' OR ')})`);
else if (terms.length === 1) queryParts.push(terms[0]);
}
// 3. Handle NOT
const notVal = getVal('inp-not');
if (notVal) {
const terms = parseList(notVal);
terms.forEach(t => queryParts.push(`-${t}`));
}
// 4. Metadata
const metadata = {
'user': 'inp-user',
'repo': 'inp-repo',
'language': 'inp-lang',
'extension': 'inp-ext',
'stars': 'inp-stars',
'forks': 'inp-forks',
'path': 'inp-path',
'topic': 'inp-topics',
'created': 'inp-created',
'pushed': 'inp-pushed',
'size': 'inp-size'
};
for (const [key, id] of Object.entries(metadata)) {
let val = getVal(id);
if (val) {
// Auto-add >= to stars/forks if missing and only a number
if ((key === 'stars' || key === 'forks') && !val.match(/[<>=]/)) val = `>=${val}`;
queryParts.push(`${key}:${val}`);
}
}
const type = document.getElementById('sel-type').value;
const sort = document.getElementById('sel-sort').value;
// Construct final URL
const finalQuery = encodeURIComponent(queryParts.join(' '));
let url = `https://github.com/search?q=${finalQuery}&type=${type}`;
if (sort) url += `&s=${sort}&o=desc`;
window.location.href = url;
}
// Init and Observe for Turbo/PJAX
createUI();
const observer = new MutationObserver(() => {
if (!document.getElementById(TRIGGER_ID)) createUI();
});
observer.observe(document.body, { childList: true, subtree: true });
})();