Skip to content

Instantly share code, notes, and snippets.

@nfrigus
Last active November 7, 2025 13:42
Show Gist options
  • Save nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d to your computer and use it in GitHub Desktop.
Save nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d to your computer and use it in GitHub Desktop.
Userscripts

Userscripts Collection

This repository contains a collection of userscripts – small pieces of JavaScript that extend or modify the behavior of websites.
They can add missing features, improve usability, or automate repetitive tasks right in your browser.


What Are Userscripts?

Userscripts are custom scripts that run in your browser when you visit specific websites.
They are managed through browser extensions such as Tampermonkey, Violentmonkey, or Greasemonkey.


How to Install

  1. Install a userscript manager:

  2. Click on a Raw button right to the scriptname you want to install (e.g., conflence.user.js).

  3. Your userscript manager will prompt you to install the script. Confirm the installation.


How to Use

  • Once installed, the userscript will run automatically whenever you visit a matching website.
  • You can manage (enable/disable/remove) your scripts via the userscript manager’s extension menu.
  • Updates are handled automatically if you install directly from this gist.

Notes

  • Scripts may target specific websites or patterns – check the script header (@match or @include) to see where it applies.
  • Always review code before installing if you’re unsure about its source.
// ==UserScript==
// @name aws-auth
// @namespace https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d
// @version 3.3.1
// @description Allow to copy credentials for every available account with a single operation
// @author [email protected]
// @match https://d-99671203c2.awsapps.com/start/
// @icon https://eu-central-1.signin.aws/favicon.ico
// @grant window.close
// @grant GM_setClipboard
// @updateURL https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/aws.user.js
// @downloadURL https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/aws.user.js
// ==/UserScript==
(function () {
'use strict'
window.addEventListener("load", load, false)
const selectors = {
accounts: '[data-testid=account-list-cell]',
modal_open_btn: '[data-testid=role-creation-action-button]',
modal_creds_block: "//span[contains(text(),'aws_access_key_id=')]/ancestor::table",
}
async function load() {
await waitForSelector(selectors.accounts)
if (!confirm('Copy credentials?')) return
const creds = []
for(const account of document.querySelectorAll(selectors.accounts)) {
account.click()
await waitForSelector(selectors.modal_open_btn, account.parentNode).then(btn => btn.click())
creds.push(await waitForXpath(selectors.modal_creds_block).then(node => node.innerText))
}
const credsStr = creds
.map(i => i.replace(/\n+/gm, "\n"))
.join("\n\n")
await navigator.clipboard.writeText(credsStr)
if (confirm(
`${creds.length} accounts processed.\n` +
`${credsStr.split("\n").length} lines are copied into clipboard.\n`)
) close()
}
async function wait(seconds = 1) {
return new Promise(resolve => setTimeout(resolve, seconds * 1e3))
}
async function waitForSelector(selector, context = document) {
return waitFor(async() => context.querySelector(selector), context)
}
async function waitForXpath(xpath, context = document) {
return waitFor(async() => document.evaluate(xpath, context, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue, context)
}
async function waitFor(handler, context = document) {
return new Promise((resolve) => {
new MutationObserver(async (_, observer) => {
const target = await handler()
if (target) {
observer.disconnect()
resolve(target)
}
}).observe(context, { childList: true, subtree: true })
})
}
function dumpTargetNodes() {
console.table([...document.querySelectorAll('[data-testid]')].map(i => [i, i.getAttribute('data-testid')]))
}
})()
// ==UserScript==
// @name Go back with backspace
// @namespace https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d
// @version 2025-11-01
// @description Use backspace to navigate to previous page
// @author [email protected]
// @match *://*/*
// @grant none
// @updateURL https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/backspace.user.js
// @downloadURL https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/backspace.user.js
// ==/UserScript==
(function() {
'use strict'
document.addEventListener('keydown', function(e) {
// We only care about Backspace
if (e.key !== 'Backspace') return
// If any modifier keys are pressed (Ctrl, Alt, Meta), skip
if (e.ctrlKey || e.altKey || e.metaKey) return
const active = document.activeElement
// Detect if the current element is editable
const isEditable =
active &&
(
active.isContentEditable ||
active.tagName === 'INPUT' ||
active.tagName === 'TEXTAREA' ||
active.getAttribute('role') === 'textbox'
)
// Detect if page is a web app that should manage Backspace itself (like Google Docs, Confluence, etc.)
const host = location.hostname
const inWebApp = /docs\.google\.com|confluence|notion\.so|figma\.com/.test(host)
// If editing or in an exempt app → do nothing
if (isEditable || inWebApp) return
// Otherwise, go back if possible
e.preventDefault()
if (history.length > 1) {
history.back();
}
}, true)
})()
// ==UserScript==
// @name Block Clipboard Tampering
// @namespace https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d
// @version 2025-11-01
// @description Prevent sites from modifying clipboard directly or by intercepting copy keyboard shortcuts
// @author [email protected]
// @match *://*/*
// @run-at document-start
// @grant none
// @updateURL https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/clipboard-guard.user.js
// @downloadURL https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/clipboard-guard.user.js
// ==/UserScript==
(function() {
'use strict'
// Protect the clipboard API early
Object.defineProperty(navigator, 'clipboard', {
value: {
writeText: () => Promise.reject('Clipboard write blocked by userscript'),
write: () => Promise.reject('Clipboard write blocked by userscript'),
readText: () => Promise.reject('Clipboard read blocked by userscript'),
read: () => Promise.reject('Clipboard read blocked by userscript'),
},
writable: false,
configurable: false
})
// Prevent sites from intercepting Ctrl+C / Cmd+C
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c') {
e.stopImmediatePropagation()
}
}, true)
// Prevent scripts from adding custom "copy" handlers
document.addEventListener('copy', function(e) {
e.stopImmediatePropagation()
}, true)
// Disable inline handlers (oncopy/onkeydown)
const observer = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.type === 'attributes' && (m.attributeName === 'oncopy' || m.attributeName === 'onkeydown')) {
m.target.removeAttribute(m.attributeName)
}
}
});
observer.observe(document.documentElement, { attributes: true, subtree: true })
console.log('[Clipboard Guard] Active on', location.hostname)
})()
// ==UserScript==
// @name confluence
// @namespace https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d
// @version 0.1.0
// @description Confluence tweaks
// @author [email protected]
// @match https://wiki.time2go.tech/*
// @icon https://wiki.time2go.tech/s/l7motr/9012/1962nzv/63/_/favicon.ico
// @run-at document-start
// @updateURL https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/confluence.user.js
// @downloadURL https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/confluence.user.js
// ==/UserScript==
(function () {
'use strict'
const { addEventListener } = EventTarget.prototype
log("Loading...")
window.addEventListener("load", load, false)
EventTarget.prototype.addEventListener = addEventListenerPatch
async function load() {}
function addEventListenerPatch(event, ...args) {
log(event)
if (event === "scroll") return
addEventListener.call(this, event, ...args)
}
function log(...args) {
console.log("[TM]", ...args)
}
function error(...args) {
console.error("[TM]", ...args)
}
function todo() {
const originalDispatch = window.dispatchEvent;
let blockScrollEvents = false;
window.dispatchEvent = function(event) {
if (blockScrollEvents && event.type === "scroll") {
return true; // skip scroll events
}
return originalDispatch.call(this, event);
};
// Usage:
blockScrollEvents = true; // pause scroll events
blockScrollEvents = false; // resume scroll events
}
})()
// ==UserScript==
// @name jira
// @namespace https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d
// @version 0.1.1
// @description Jira tools
// @icon https://jira.time2go.tech/s/9urhrs/820030/1dlckms/_/images/fav-generic.png
// @author [email protected]
// @match https://jira.time2go.tech/*
// @grant unsafeWindow
// @updateURL https://gist.githubusercontent.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/jira.user.js
// @downloadURL https://gist.githubusercontent.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/jira.user.js
// ==/UserScript==
(function () {
'use strict'
unsafeWindow.submitTimeLog = submitTimeLog
async function mimicUserInput(el, text) {
let set
if(el instanceof HTMLInputElement) {
set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set
} else if (el instanceof HTMLTextAreaElement) {
set = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value").set
} else {
throw new Error("Unsupported input type")
}
el.focus()
for (const ch of text) {
el.dispatchEvent(new KeyboardEvent("keydown", { key: ch, bubbles: true }))
set.call(el, (el.value ?? "") + ch)
el.dispatchEvent(new InputEvent("beforeinput", { inputType: "insertText", data: ch, bubbles: true, cancelable: true }))
el.dispatchEvent(new InputEvent("input", { inputType: "insertText", data: ch, bubbles: true, composed: true }))
el.dispatchEvent(new KeyboardEvent("keyup", { key: ch, bubbles: true }))
}
el.blur()
el.dispatchEvent(new Event('change', { bubbles: true }))
await Promise.resolve()
}
async function submitTimeLogGui(date, issue, spent, description) {
// document.querySelector('[name="tempoCalendarLogWork"]').click() todo: add date select
document.getElementById(date).querySelector('[name=tempoCalendarLogWork]').click()
const issuePickerInput = await waitForSelector("#issuePickerInput")
issuePickerInput.parentNode.childNodes[0].click()
mimicUserInput(issuePickerInput, issue)
const issuePickerDropdown = await waitForSelector(`#${issue}-search-1-row`)
issuePickerDropdown.click()
mimicUserInput(document.getElementById("timeSpentSeconds"), spent)
mimicUserInput(document.getElementById("description"), description)
await wait()
document.getElementsByName("submitWorklogButton")[0].click()
}
async function wait(seconds = 1) {
return new Promise(resolve => setTimeout(resolve, seconds * 1e3))
}
async function waitForSelector(selector, context = document) {
log("waitForSelector", selector)
return waitFor(async () => context.querySelector(selector), context)
}
async function waitForXpath(xpath, context = document) {
return waitFor(async () => document.evaluate(xpath, context, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue, context)
}
async function waitFor(handler, context = document) {
return new Promise((resolve) => {
new MutationObserver(async (_, observer) => {
const target = await handler()
if (target) {
observer.disconnect()
resolve(target)
}
}).observe(context, {
childList: true,
subtree: true
})
})
}
function log(...args) {
console.log("[sm]", ...args)
}
async function getIssueId(issue) {
return fetch("https://jira.time2go.tech/rest/api/2/search/", {
"headers": {
"accept": "application/json, application/vnd-ms-excel",
"content-type": "application/json"
},
"body": JSON.stringify({
"jql": `key = "${issue}"`,
"fields": [],
"startAt": 0,
"maxResults": 1,
"validateQuery": false
}),
"method": "POST",
"mode": "cors",
"credentials": "include"
}).then(res => res.json()).then(result => result.issues[0].id)
}
async function submitTimeLog(date, issue, spent, description) {
const id = await getIssueId(issue)
await fetch("https://jira.time2go.tech/rest/tempo-timesheets/4/worklogs/", {
"headers": { "content-type": "application/json" },
"referrer": "https://jira.time2go.tech/secure/Tempo.jspa",
"body": JSON.stringify({
"attributes": {},
"billableSeconds": "",
"originId": -1,
"worker": document.getElementsByName("ajs-tempo-user-key")[0].content,
"comment": description,
"started": date,
"timeSpentSeconds": +spent.replace("h", "") * 3600,
"originTaskId": id,
"remainingEstimate": 0,
"endDate": null,
"includeNonWorkingDays": false
}),
"method": "POST",
"mode": "cors",
"credentials": "include"
})
}
})()
// ==UserScript==
// @name seasonvar
// @namespace https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d
// @version 2025-09-02
// @description UI tweaks: 1. Dim unavailable items in "Хочу посмотреть" section; 2. Highlight search marginal bar items according to their watch status.
// @author [email protected]
// @match https://seasonvar.ru/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=seasonvar.ru
// @grant none
// @updateURL https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/seasonvar.user.js
// @downloadURL https://gist.github.com/nfrigus/a026c7dfe4fb79b3373d2fcf1571f52d/raw/seasonvar.user.js
// ==/UserScript==
(function() {
'use strict'
const css = `
/* Landing search results */
.lside-serial a:has(.svico-mwatch) { color: red; }
.lside-serial a:has(.svico-mwatched) { opacity: .2; }
.lside-serial a:has(.svico-mnotwatch) { opacity: .1; }
.lside-serial a[data-trailer] { color: blue; }
/* Watch list */
.pgs-marks-el a.block { opacity: .1; }
.trailer-only { opacity: .1; }
.trailer-only:hover { opacity: 1; }
`
window.addEventListener("load", load, false)
async function load() {
attachStyles()
new MutationObserver(async (_, observer) => {
markTrailerOnly()
}).observe(document, { childList: true, subtree: true })
}
function attachStyles() {
const style = document.createElement('style')
style.textContent = css
document.head.appendChild(style)
}
function markTrailerOnly() {
let result = document.evaluate(
"//div[contains(@class, 'pgs-marks-el')][.//strong[text()='Только трейлер']]",
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (let i = 0; i < result.snapshotLength; i++) {
result.snapshotItem(i).classList.add("trailer-only")
}
}
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment