A userscript to help avoid getting blasted by a wall of white light while hacking at 2am
1
auto-dark-mode.user.js
edited
1// ==UserScript==
2// @name Auto Dark Mode
3// @namespace mydarkmode
4// @version 1.0
5// @description Automatically invert light-themed pages when system prefers dark mode
6// @author snailbird.bsky.social
7// @match *://*/*
8// @exclude https://mail.google.com/*
9// @grant GM_addStyle
10// @run-at document-idle
11// ==/UserScript==
12
13(function () {
14 'use strict';
15
16 const LUMINANCE_THRESHOLD = 0.4;
17 const RECHECK_DELAY_MS = 1500;
18 const CLASS_NAME = 'jsnl-dark-mode';
19
20 const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
21
22 function srgbToLinear(c) {
23 c /= 255;
24 return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
25 }
26
27 function relativeLuminance(r, g, b) {
28 return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
29 }
30
31 function parseRgb(color) {
32 if (!color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)') return null;
33 const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
34 return m ? [+m[1], +m[2], +m[3]] : null;
35 }
36
37 function getPageLuminance() {
38 for (const el of [document.body, document.documentElement]) {
39 if (!el) continue;
40 const rgb = parseRgb(window.getComputedStyle(el).backgroundColor);
41 if (rgb) return relativeLuminance(...rgb);
42 }
43 return 1;
44 }
45
46 let styleInjected = false;
47
48 function injectStyle() {
49 if (styleInjected) return;
50 styleInjected = true;
51 GM_addStyle(`
52 .${CLASS_NAME} {
53 filter: invert(0.90) hue-rotate(180deg);
54 }
55 .${CLASS_NAME} img,
56 .${CLASS_NAME} video,
57 .${CLASS_NAME} svg image,
58 .${CLASS_NAME} picture,
59 .${CLASS_NAME} [style*="background-image"] {
60 filter: invert(1) hue-rotate(180deg);
61 }
62 `);
63 }
64
65 function activate() {
66 injectStyle();
67 (document.documentElement ?? document.body).classList.add(CLASS_NAME);
68 }
69
70 function deactivate() {
71 (document.documentElement ?? document.body).classList.remove(CLASS_NAME);
72 }
73
74 function evaluate() {
75 if (!darkModeQuery.matches) {
76 deactivate();
77 return;
78 }
79 if (getPageLuminance() >= LUMINANCE_THRESHOLD) {
80 activate();
81 }
82 }
83
84 evaluate();
85 setTimeout(evaluate, RECHECK_DELAY_MS);
86
87 darkModeQuery.addEventListener('change', evaluate);
88})();