From d6f032e52e91c2722077b8d4acae25f08c24a342 Mon Sep 17 00:00:00 2001 From: Jet Date: Sun, 17 May 2026 15:27:14 -0700 Subject: [PATCH] feat: add evil bit support --- gnome-extensions/evil-bit-toggle/extension.js | 151 ++++++++++++++++++ .../evil-bit-toggle/metadata.json | 6 + home-modules/desktop.nix | 1 + home-modules/packages.nix | 81 ++++++++++ 4 files changed, 239 insertions(+) create mode 100644 gnome-extensions/evil-bit-toggle/extension.js create mode 100644 gnome-extensions/evil-bit-toggle/metadata.json diff --git a/gnome-extensions/evil-bit-toggle/extension.js b/gnome-extensions/evil-bit-toggle/extension.js new file mode 100644 index 0000000..ca2346c --- /dev/null +++ b/gnome-extensions/evil-bit-toggle/extension.js @@ -0,0 +1,151 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; + +import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js'; + +const EVIL_BIT_CTL = '@evilBitCtl@'; +const PKEXEC = '/run/wrappers/bin/pkexec'; +const STATE_FILE = '/run/evil-bit-toggle/enabled'; +const ICON_NAME = 'dialog-warning-symbolic'; + +const EvilBitToggle = GObject.registerClass( +class EvilBitToggle extends QuickSettings.QuickToggle { + constructor() { + super({ + title: 'Evil Bit', + subtitle: 'RFC 3514: good', + iconName: ICON_NAME, + toggleMode: true, + }); + + this._syncing = false; + this._pending = false; + this._destroyed = false; + this._syncFromState(); + + this._checkedId = this.connect('notify::checked', () => { + if (this._syncing) + return; + + this._setEvilBit(this.checked); + }); + } + + _isEnabled() { + return GLib.file_test(STATE_FILE, GLib.FileTest.EXISTS); + } + + _syncFromState() { + this._syncing = true; + this.checked = this._isEnabled(); + this._syncing = false; + this._syncSubtitle(); + } + + _setEvilBit(enabled) { + if (this._pending) + return; + + this._pending = true; + this.reactive = false; + this.subtitle = 'Applying...'; + + const action = enabled ? 'enable' : 'disable'; + let proc; + + try { + proc = Gio.Subprocess.new( + [PKEXEC, EVIL_BIT_CTL, action], + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE + ); + } catch (error) { + this._finishSetEvilBit(error); + return; + } + + proc.communicate_utf8_async(null, null, (subprocess, result) => { + try { + const [, , stderr] = subprocess.communicate_utf8_finish(result); + + if (!subprocess.get_successful()) + throw new Error(stderr.trim() || `evil-bitctl ${action} failed`); + + this._finishSetEvilBit(null); + } catch (error) { + this._finishSetEvilBit(error); + } + }); + } + + _finishSetEvilBit(error) { + if (error) + console.warn(`Unable to toggle Evil Bit: ${error.message}`); + + if (this._destroyed) + return; + + this._pending = false; + this.reactive = true; + this._syncFromState(); + } + + _syncSubtitle() { + this.subtitle = this.checked ? 'RFC 3514: evil' : 'RFC 3514: good'; + } + + destroy() { + this._destroyed = true; + + if (this._checkedId) { + this.disconnect(this._checkedId); + this._checkedId = null; + } + + super.destroy(); + } +}); + +const EvilBitIndicator = GObject.registerClass( +class EvilBitIndicator extends QuickSettings.SystemIndicator { + constructor() { + super(); + + this._indicator = this._addIndicator(); + this._indicator.icon_name = ICON_NAME; + + this._toggle = new EvilBitToggle(); + this._indicator.visible = this._toggle.checked; + this._checkedId = this._toggle.connect('notify::checked', () => { + this._indicator.visible = this._toggle.checked; + }); + + this.quickSettingsItems.push(this._toggle); + } + + destroy() { + if (this._checkedId) { + this._toggle.disconnect(this._checkedId); + this._checkedId = null; + } + + this.quickSettingsItems.forEach(item => item.destroy()); + this._toggle = null; + + super.destroy(); + } +}); + +export default class EvilBitToggleExtension extends Extension { + enable() { + this._indicator = new EvilBitIndicator(); + Main.panel.statusArea.quickSettings.addExternalIndicator(this._indicator); + } + + disable() { + this._indicator.destroy(); + this._indicator = null; + } +} diff --git a/gnome-extensions/evil-bit-toggle/metadata.json b/gnome-extensions/evil-bit-toggle/metadata.json new file mode 100644 index 0000000..5b78e56 --- /dev/null +++ b/gnome-extensions/evil-bit-toggle/metadata.json @@ -0,0 +1,6 @@ +{ + "uuid": "evil-bit-toggle@jetpham.github.com", + "name": "Evil Bit Toggle", + "description": "Adds a Quick Settings toggle for the RFC 3514 evil bit.", + "shell-version": ["49"] +} diff --git a/home-modules/desktop.nix b/home-modules/desktop.nix index e84261c..5cba10c 100644 --- a/home-modules/desktop.nix +++ b/home-modules/desktop.nix @@ -182,6 +182,7 @@ in "appindicatorsupport@rgcjonas.gmail.com" "gnome-shell-extension-maximized-by-default@stiggimy.github.com" "no-titlebar-when-maximized@alec.ninja" + "evil-bit-toggle@jetpham.github.com" "reduced-motion-toggle@jetpham.github.com" ]; favorite-apps = favoriteApps; diff --git a/home-modules/packages.nix b/home-modules/packages.nix index 72200c2..67a740b 100644 --- a/home-modules/packages.nix +++ b/home-modules/packages.nix @@ -6,6 +6,68 @@ }: let + evilBitCtl = pkgs.writeShellApplication { + name = "evil-bitctl"; + runtimeInputs = [ + pkgs.coreutils + pkgs.nftables + ]; + text = '' + state_dir=/run/evil-bit-toggle + state_file="$state_dir/enabled" + table=evil_bit + chain=output + + usage() { + printf 'Usage: evil-bitctl {enable|disable|status}\n' >&2 + exit 64 + } + + enable() { + nft add table ip "$table" 2>/dev/null || true + + if nft list chain ip "$table" "$chain" >/dev/null 2>&1; then + nft flush chain ip "$table" "$chain" + else + nft add chain ip "$table" "$chain" '{ type route hook output priority mangle; policy accept; }' + fi + + nft add rule ip "$table" "$chain" ip frag-off set ip frag-off '|' 0x8000 + install -d -m 0755 "$state_dir" + touch "$state_file" + } + + disable() { + nft delete table ip "$table" 2>/dev/null || true + rm -f "$state_file" + rmdir "$state_dir" 2>/dev/null || true + } + + status() { + if [ -e "$state_file" ] && nft list table ip "$table" >/dev/null 2>&1; then + printf 'enabled\n' + else + printf 'disabled\n' + fi + } + + case "''${1:-}" in + enable) + enable + ;; + disable) + disable + ;; + status) + status + ;; + *) + usage + ;; + esac + ''; + }; + reducedMotionToggleExtension = pkgs.stdenvNoCC.mkDerivation { pname = "gnome-shell-extension-reduced-motion-toggle"; version = "1"; @@ -20,6 +82,24 @@ let runHook postInstall ''; }; + + evilBitToggleExtension = pkgs.stdenvNoCC.mkDerivation { + pname = "gnome-shell-extension-evil-bit-toggle"; + version = "1"; + src = ../gnome-extensions/evil-bit-toggle; + + installPhase = '' + runHook preInstall + + substituteInPlace extension.js \ + --replace-fail @evilBitCtl@ ${evilBitCtl}/bin/evil-bitctl + + mkdir -p "$out/share/gnome-shell/extensions/evil-bit-toggle@jetpham.github.com" + cp -r . "$out/share/gnome-shell/extensions/evil-bit-toggle@jetpham.github.com" + + runHook postInstall + ''; + }; in { @@ -105,6 +185,7 @@ in gnomeExtensions.system-monitor-next gnomeExtensions.tailscale-qs gnomeExtensions.wifi-qrcode + evilBitToggleExtension reducedMotionToggleExtension nerd-fonts.commit-mono